Java类加载器

classloader顾名思义,即是类加载。虚拟机把描述类的数据从class字节码文件加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

先认识一下类加载器在jvm中所处的位置

它在JVM外部,负责将class文件,解析成JVM能识别的Java的类

类加载器ClassLoader中它生命周期包括加载、链接、初始化

链接又分为 验证,准备,解析三个部分

很抽象没关系,我们有口诀“家宴准备了西式菜”,即家(加载)宴(验证)准备 (准备)了西(解析)式(初始化)菜。

生命周期包括:类加载、验证、准备、解析、初始化、使用和卸载七个阶段。

《深入理解JVM》有详细介绍

虽然classloader的加载过程有复杂的5步,但事实上除了加载之外的四步,其它都是由JVM虚拟机控制的,我 们除了适应它的规范进行开发外,能够干预的空间并不多。而加载则是我们控制classloader实现特殊目的最重 要的手段了。也是接下来我们介绍的重点了。

用加载器可以完成什么工作呢

1.加载类

这当然是它最本职也是最基础的工作

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。

  • 本地类的加载是优先于jar包类的加载的,之前启动应用碰到一个问题。我启动的A应用,在启动时候会去请求B应用,但是B应用的这个接口已经下线了,没有返回结果就启动失败,我们可以通过在自己应用同包名下创建一个同名类,剔除掉引用第三方接口的数据,这样类加载的时候先从本地文件加载,就解决了应用无法启动的问题。
  • 从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
  • 通过网络加载class文件。
  • 把一个Java源文件动态编译,并执行加载。

2.保证Java程序的稳定运作

3.解决依赖冲突

4.热加载

5.热部署

6.加密保护

下面逐个介绍这些功能是怎么被实现的。

首先要引入一个概念——双亲委派机制

这是Java设计者们推荐给开发者的一种类加载器实现的最佳实践。

(本文只讨论JDK9以前的类加载,对于9及9以后的,自定义类加载器部分也适用),

JDK9以前Java应用都是由三种类加载器互相配合来完成加载的,而这三种加载器就是通过双亲委派机制来加载的。

双亲委派机制

1.加载类

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

  • 扩展内容 下面是jdk9的示意图

说人话就是 加载器拿到类加载请求先给爸爸干,爸爸拿到请求先给爷爷干,爷爷干不了的爸爸干,爸爸干不了的自己干,有自定义加载器也是这个顺序类推就行。这样就统一加载顺序,先加载根类,再加载扩展类,再加载应用类,再加载自定义的类。

它的实现却非常简单,用以实现双亲委派的代码只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中,

    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 首先,检查请求的类是否已经被加载过了Class c = findLoadedClass(name);if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 如果父类加载器抛出ClassNotFoundException// 说明父类加载器无法完成加载请求}if (c == null) {// 在父类加载器无法加载时// 再调用本身的findClass方法来进行类加载c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
}

2.保证Java程序的稳定运作

双亲委派模型对于保证Java程序的稳定运作极为重要,

使用双亲委派模型来组织类加载器之间的关系,Java类会随着它的加载器一起具备了一种带有优先级的层次关系,通过这种层次模型,可以避免类的重复加载,也可以避免核心 类被不同的类加载器加载到内存中造成冲突和混乱,从而保证了Java核心库的安全。

说人话就是 确定内存中类的唯一性

这样哪怕自己也写一个String类,类加载器也会加载jdk中rt.jar中的String类,避免恶意代码植入,避免引入时无法区分加载哪个类。

我明明是在自己写的String里面执行main方法,却提示找不到main方法,说明真实加载的类其实是原本的String类,故而没有main方法,这样就保护了jdk中本身存在的类不会被恶意篡改。

扩展:唯一性 任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性

比如我们平时定义的类也是通过包名+类名确定唯一性的,其实这是默认在同一类加载器下

/*** 类加载器与instanceof关键字演示*/
public class ClassLoaderTest {public static void main(String[] args) throws Exception {ClassLoader myLoader = new ClassLoader() {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {try {String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";InputStream is = getClass().getResourceAsStream(fileName);if (is == null) {return super.loadClass(name);}byte[] b = new byte[is.available()];is.read(b);return defineClass(name, b, 0, b.length);} catch (IOException e) {throw new ClassNotFoundException(name);}}};Object obj = myLoader.loadClass("com.example.classloader.ClassLoaderTest").newInstance();System.out.println(obj.getClass());System.out.println(obj instanceof com.example.classloader.ClassLoaderTest );System.out.println(obj.getClass().equals(com.example.classloader.ClassLoaderTest.class));}
}
运行结果:
class com.example.classloader.ClassLoaderTest
false
false

例子中,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,但只要加载它们的类加载器不同,那这两个类就必定不相等。

从第一行可以看到这个对象确实是类com.example.classloader.ClassLoaderTest实例化出来的,但在第二行的输出中却发现这个对象与类com.example.classloader.ClassLoaderTest做所属 类型检查的时候返回了false。这是因为Java虚拟机中同时存在了两个ClassLoaderTest类,一个是由虚拟机的应用程序类加载器所加载的,另外一个是由我们自定义的类加载器加载的,虽然它们都来自同一 个Class文件,但在Java虚拟机中仍然是两个互相独立的类,做对象所属类型检查时的结果自然为 false,第三行false也是同理。

非双亲委派机制

3.解决依赖冲突

相信只要用过maven协同开发大型项目的同学都有过这个苦恼,基于maven的pom进制可以方便的进行依赖管理,但是由于maven依赖的传递性,会导致我们的依赖错综复杂,这样就会导致引入类冲突的问题。最典型的就是 NoSuchMethodError错误。

可以参考我之前写的博文

https://blog.csdn.net/wdays83892469/article/details/117204426?spm=1001.2014.3001.5501

那么当一个项目引入不同的中间件的时候,很容易就会带入一些不一样版本的依赖,比如引用fastjson版本不一致。场景如下

某个业务引用了消息中间件(例如RabbitMQ)和微服务中间件(例如dubbo),这两个中间件也同时引用了 fastjson-2.0和fastjson-3.0版本,而业务自己本身也引用了fastjson-1.0版本。这三个版本表现不同之处在于classA类中方法数目不相同,我们根据maven依赖处理的机制,引用路径最短的fastjson-1.0会真正作为应 用最终的依赖,其它两个版本的fastjson则会被忽略,那么中间件在调用method2()方法的时候,则会抛出方法 找不到异常。

或许你会说,将所有依赖fastjson的版本都升级到3.0不是就能解解决问题吗?确实这样能够解决 问题,但是在实际操作中不太现实,首先,中间件团队和业务团队之间并不是一个团队,并不能做到高效协同,其次是中间件的稳定性是需要保障的,不可能因为包冲突问题,就升级版本,更何况一个中间件依赖的包可能有 上百个,如果纯粹依赖包升级来解决,不仅稳定性难以保障,排包耗费的时间恐怕就让人窒息了。

那如何解决包冲突的问题呢?答案就是pandora(潘多拉),通过自定义类加载器,为每个中间件自定义一个加载器,这些加载器之间的关系是平行的,彼此没有依赖关系。这样每个中间件的classloader就可以加载各自版本 的fastjson。因为一个类的全限定名以及加载该类的加载器两者共同形成了这个类在JVM中的惟一标识,这也是阿里pandora实现依赖隔离的基础。

可能到这里,你又会有新的疑惑,根据双亲委托模型,App Classloader分别继承了Custom Classloader.那 么业务包中的fastjson的class在加载的时候,会先委托到Custom ClassLoader。这样不就会导致自身依赖 的fastjson版本被忽略吗?确实如此,所以潘多拉又是如何做的呢?

现在把视野回到一开始提过的一句 双亲委派机制是Java设计者们推荐给开发者的一种类加载器实现的最佳实践。

也就是说,这只是一个推荐,和饮料的建议零售价是一个道理,我可以自定义顺序去加载类,这样就避免了自身依赖的fastjson版本被忽略。

比如我们可以重新定义一个exportedClassHashMap,用于存放中间件使用到的类,让应用程序类加载器首先使用exportedClassHashMap来加载类。如果exportedClassHashMap没有加载到再使用默认的双亲委派加载。

阿里实现如图所示

   /**classLoader源码**/protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 首先检查这个类是否已经被加载了Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 如果父类加载器抛出ClassNotFoundException// 说明父类加载器无法完成加载请求}if (c == null) {// 在父类加载器无法加载时// 再调用本身的findClass方法来进行类加载long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
   /**如下改造即可**/@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 导出类中是否存在,如果存在则直接返回if (classCache != null && classCache.containsKey(name)) {return classCache.get(name);}// 双亲委派机制加载return super.loadClass(name, resolve);}

4.热加载

在开发项目的时候,我们需要频繁的重启应用进行程序调试,但是java项目的启动少则几十秒,多则几分钟。 如此慢的启动速度极大地影响了程序开发的效率,那是否可以快速的进行启动,进而能够快速的进行开发验证 呢?答案也是肯定的,通过classloader我们可以完成对变更内容的加载,然后快速的启动。 常用的热加载方案有好几个,接下来我们介绍下spring官方推荐的热加载方案,即spring boot devtools。

首先我们需要思考下,为什么重新启动一个应用会比较慢,那是因为在启动应用的时候,JVM虚拟机需要将所有的 应用程序重新装载到整个虚拟机。可想而知,一个复杂的应用程序所包含的jar包可能有上百兆,每次微小的改动都 是全量加载,那自然是很慢了。那么我们是否可以做到,当我们修改了某个文件后,在JVM中替换到这个文件相关 的部分而不全量的重新加载呢?而spring boot devtools正是基于这个思路进行处理的。

如上图所示,通常一个项目的代码由以上四部分组成,即基础类、扩展类、二方包/三方包、以及我们自己编写的 业务代码组成。上面的一排是我们通常的类加载结构,其中业务代码和二方包/三方包是由应用加载器加载的。而 实际开发和调试的过程中,主要变化的是业务代码,并且业务代码相对二方包/三方包的内容来说会更少一些。因 此我们可以将业务代码单独通过一个自定义的加载器Custom Classloader来进行加载,当监控发现业务代码发生 改变后,我们重新加载启动,老的业务代码的相关类则由虚拟机的垃圾回收机制来自动回收。其工程流程大概如 下。有兴趣的同学可以去看下源码,会更加清楚。

RestartClassLoader为自定义的类加载器,其核心是loadClass的加载方式,我们发现其通过修改了双亲委托机制,默认优先从自己加载,如果自己没有加载到,从从parent进行加载。

这样保证了业务代码可以优先被 RestartClassLoader加载。

进而通过重新加载RestartClassLoader即可完成应用代码部分的重新加载。

protected Class<?> loadClassz`(String name, boolean resolve)throws ClassNotFoundException {String path = name.replace('.','/').concat(".class");ClassLoaderFile file = this.updatedFiles.getFile(path);if (file != null && file.getKind() == Kind.DELETED) {throw new ClassNotFoundException();}synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> loadedClass = findLoadedClass(name);if (loadedClass == null) {try {// 优先从自己加载loadedClass = findClass(name);}catch (ClassNotFoundException exception){// 如果没有加载到,则从父类加载loadedClass = Class.forName(name,false,getParent());}}if (resolve) {resolveClass(loadedClass);}return loadedClass;}}

5.热部署

热部署本质其实与热加载并没有太大的区别,通常我们说热加载是指在开发环境中进行的classloader加载,而热 部署则更多是指在线上环境使用classloader的加载机制完成业务的部署。所以这二者使用的技术并没有本质的区 别。那热部署除了与热加载具有发布更快之外,还有更多的更大的优势就是具有更细的发布粒度。我们可以想像以 下的一个业务场景。

假设某个营销投放平台涉及到4个业务方的开发,需要对会场业务进行投放。而这四个业务方的代码全部都在一个应 用里面。因此某个业务方有代码变更则需要对整个应用进行发布,同时其它业务方也需要跟着回归。因此每个微小的发动,则需要走整个应用的全量发布。这种方式带来的稳定性风险估且不说,整个发布迭代的效率也可想而知了。这在整个互联网里,时间和效率就是金钱的理念下,显然是无法接受的。 那么我们完全可以通过类加载机制,将每个业务方通过一个classloader来加载。基于类的隔离机制,可以保障各个业务方的代码不会相互影响,同时也可以做到各个业务方进行独立的发布。其实在移动客户端,每个应用模块也可以基于类加载,实现插件化发布。本质上也是一个原理。 在阿里内部像阿拉丁投放平台,以及crossbow容器化平台,本质都是使用classloader的热加载技术,实现业务细粒度的开发部署以及多应用的合并部署。

6.加密保护

众所周期,基于java开发编译产生的jar包是由.class字节码组成,由于字节码的文件格式是有明确规范的。因此对 于字节码进行反编译,就很容易知道其源码实现了。因此大致会存在如下两个方面的诉求。例如在服务端,我们向 别人提供三方包实现的时候,不希望别人知道核心代码实现,我们可以考虑对jar包进行加密,在客户端则会比较普 遍,那就是我们打包好的apk的安装包,不希望被人家反编译而被人家翻个底朝天,我们也可以对apk进行加密。 jar包加密的本质,还是对字节码文件进行操作。但是JVM虚拟机加载class的规范是统一的,因此我们在最终加载 class文件的时候,还是需要满足其class文件的格式规范,否则虚拟机是不能正常加载的。因此我们可以在打包的 时候对class进行正向的加密操作,然后,在加载class文件之前通过自定义classloader先进行反向的解密操作,然 后再按照标准的class文件标准进行加载,这样就完成了class文件正常的加载。因此这个加密的jar包只有能够实现 解密方法的classloader才能正常加载。

简单的实现方案

protected Class<?> loadClass2(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {Class clazz = findLoadedClass(name);if (clazz != null) {return clazz;}// 提前对class文件进行解密Class<?> loadedClass = findLoadedClass(name);try {// 读取经过加密的类文件byte classData[] = Util.readFile("name." + class);if (classData != null) {// 解密byte decryedClassData[] = ciper.doFinal(classData);// 转成一个类clazz = defineClass( name, decryedClassData, 0,decryedClassData.length);}}catch (ClassNotFoundException e){e.printStackTrace();}// 必须的步骤2// 尝试用默认的ClassLoader装入它if (resolve && clazz != null) {clazz = findSystemClass(name);}if (resolve) {resolveClass(loadedClass);}return loadedClass;}}

这样整个jar包的安全性就有一定程度的提高,至于更高安全的保障则取决于加密算法的安全性了以及如何保障 加密算法的密钥不被泄露的问题了。这有种套娃的感觉,所谓安全基本都是相对的。并且这些方法也不是绝对 的,例如可以通过对classloader进行插码,对解密后的class文件进行存储;另外大多数JVM本身并不安全, 还可以修改JVM,从ClassLoader之外获取解密后的代码并保存到磁盘,从而绕过上述加密所做的一切工作, 当然这些操作的成本就比单纯的class反编译就高很多了。所以说安全保障只要做到使对方破解的成本高于收益 即是安全,所以一定程度的安全性,足以减少很多低成本的攻击了。

本文对classloader的加载过程和加载原理进行了介绍,并结合类加载机制的特征,介绍了其相应的使用场景。 由于篇幅限制,并没有对每种场景的具体实现细节进行介绍,而只是阐述了其基本实现思路。或许大家觉得 classloader的应用有些复杂,但事实上只要大家对class从哪里加载,搞清楚loadClass的机制,就已经成功 了一大半。正所谓万变不离其宗,抓住了本质,其它问题也就迎刃而解了。

参考资料

《码出高效:Java开发手册》 杨冠宝(孤尽) 高海慧(鸣莎) p100

《深入理解Java虚拟机:JVM高级特性与最佳实践》(第3版)7.4 类加载器

Java类加载器 — classloader 的原理及应用 金雅博(行泽) 出品:淘系技术

https://www.bilibili.com/video/BV1iJ411d7jS?p=3 狂神说Java

公众号:欢迎关注,分享读书笔记,JAVA技术

下篇简单介绍几种类加载器的区别:预告如下
只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[1],是虚拟机自身的一部分;另外一种就是其他所有 的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

Java类加载器的使用相关推荐

  1. 深入探讨 Java 类加载器

    深入探讨 Java 类加载器 类加载器(class loader)是 Java™中的一个很重要的概念.类加载器负责加载 Java 类的字节代码到 Java 虚拟机中.本文首先详细介绍了 Java 类加 ...

  2. 80070583类不存在_结合JVM源码谈Java类加载器

    一.前言 之前文章 加多:ClassLoader解惑​zhuanlan.zhihu.com 从Java层面讲解了Java类加载器的原理,这里我们结合JVM源码在稍微深入讲解下. 二.Java类加载器的 ...

  3. java类加载器_类加载器

    回顾一下类加载过程 类加载过程:加载->连接->初始化.连接过程又可分为三步:验证->准备->解析. 一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的 ...

  4. java 类加载器 解密_JAVA类加载器总结整理

    一.What(是什么?) 1.概念 Java类加载器是Java运行时环境的一部分,负责动态加载Java类到JVM的内存空间中.每个Java类必须由某个类加载器装入到内存中.每一个类加载器都有一个父类加 ...

  5. Java类加载器总结

    转载自  Java类加载器总结 1.类的加载过程   JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤,如下图所示: 1) 装载:查 ...

  6. java 调用scala 类_如何使用java类加载器调用带参数的scala函数?

    我正在寻找一些将scala jar加载到java类加载器的指导. 当我使用java jar文件时,下面的函数对我有效. 其中,arr是一个java.net.URL数组,用于我需要加载到类加载器中的所有 ...

  7. Java类加载器( 死磕9)

    [正文]Java类加载器(  CLassLoader ) 死磕9:  上下文加载器原理和案例 本小节目录 9.1. 父加载器不能访问子加载器的类 9.2. 一个宠物工厂接口 9.3. 一个宠物工厂管理 ...

  8. java 类加载器-基础

    java 类加载器-基础 类加载机制 类加载器的双亲委托机制 自定义类加载路径 自定义类加载器 类加载机制 类加载器ClassLoader. – 负责查找,加载,校验字节码的应用程序. – java. ...

  9. 【java】 Java 类加载器 破坏双亲委派

    1.概述 深入探讨 Java 类加载器 Java双亲委派模型:为什么要双亲委派?如何打破它?破在哪里? Java中的双亲委派机制以及如何打破

  10. java类加载器正确的是_Java面试题:面向对象,类加载器,JDBC, Spring 基础概念

    1. 为什么说Java是一门平台无关语言? 平台无关实际的含义是"一次编写到处运行".Java 能够做到是因为它的字节码(byte code)可以运行在任何操作系统上,与底层系统无 ...

最新文章

  1. 《Redis官方文档》用Redis构建分布式锁(悲观锁)
  2. 12. 定义类 【连载 12】
  3. 项目怎么部署到服务器上_项目开发学习 云服务器的部署
  4. js条件语句初步练习
  5. linux oracle手动启动两个实例
  6. 动手造轮子:实现一个简单的依赖注入(零)
  7. 台式计算机欢迎界面下不去,Win7系统开机不显示欢迎界面的方法
  8. 将GPIO外设挂到Cortex_M3 AHB总线上详细流程扩展外设步骤总结
  9. linux系统资格认证,Linux资格认证:Linux操作系统的运行级别
  10. classnotfoundexception是什么异常_大佬说“异常信息”是优秀程序员编写代码的宝贵财富,这是真的吗...
  11. idea加载jsp项目无法打开web页面
  12. extremecomponents 配置
  13. hp服务器经常自动重启,惠普电脑经常自动重启的解决方法
  14. 杂谈:珍藏炙热的火种,静候东风
  15. 【工作记录】VRLe工作总结
  16. yarn打包报错:error during build: Error: Assigning to rvalue (Note that you need plugins to import files
  17. Android如何让Fragment加载到Activity中
  18. 【回忆杀】程序员书房翻杂物,看到初恋的信件,看到奖牌,看到梅西、力宏,他的眼角又湿了.....
  19. php ses 发送邮件,Amazon SES – 通过PHP sdk发送HTML邮件
  20. 程序人生:hello程序的P2P

热门文章

  1. 数据终端设备与无线通信模块之间串行通信链路复用协议(TS27.010)在嵌入式系统上的开发【转】...
  2. 数据类产品设计和实现思路
  3. ADMM随堂笔记(3):一致和协同
  4. HTML5响应式手机模板:H5网站设计——政府街道社区wap微信官网模板(11个页面) HTML+CSS+JavaScript 手机模板wap 手机网站模板 企业手机网站模板
  5. SQL 基础教程 (第2版)
  6. Visual Studio 2015(C#)编写实现TCP调试助手(服务端+客户端一体)-新手
  7. retainAll()和removeAll()
  8. 通过描述系统的微分方程,判断系统是否为线性系统以及是定常系统还是时变系统
  9. Matlab绘制散点图(scatter函数)
  10. 点播的时候出现 “S1-10921” 的错误