每日一读
人生就像一场旅行,我们在路上遇见了各种各样的人和事。有些人只是匆匆路过,有些人却成为了我们生命中的重要角色。他们或许给我们带来欢笑,或许给我们带来泪水,但无论如何,他们都是我们旅途中的风景。有时候,我们会迷失方向,感到迷茫和孤独,但只要我们坚持走下去,就一定能找到属于自己的归宿。
正文
深入JDK源码详解Java类加载机制:双亲委派模型的实现原理(一)
上一篇文章主要说明的是jvm中这几个核心的类加载器到底是怎么加载的,经过一系列的调试,翻底层代码以及示例代码
双亲委派机制
jvm类加载器是有亲子层级结构的 ,如下图

这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再 委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的 类加载路径中查找并载入目标类。比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载 器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天 没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的 类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器, 应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载
为什么是从应用程序类加载器加载的?
会不会有这样的疑问,就是为什么Math类是从应用程序类加载器加载的,而不是引导类加载器?
简单看一下源码
C++调用java代码生成一个jvm加载器实例,然后调用getClassLoader()方法,如下,所以,我们只需要明白this.loader是什么就可以理解了

通过源码可以发现,loader就是应用程序类加载器,所以是从应用程序类加载器加载的,这个是JVM实现的

那么为什么jvm是这样实现的呢?直接从根节点加载不是更好吗?为什么非要从应用程序类加载器开始加载,如果没有加载到就向上委托,如果引导类加载器没有加载到,那么又向下委托,这样不是更麻烦了吗?
其实这个思想或者设计只有jvm的开发人员能明白,我们更多的是猜测
我的想法是这样的(如果有其它想法也可以评论区讨论,随时欢迎):
对于一个web应用程序来说,你开发出来的代码,真正要运行的代码,95%的代码都是应用程序类加载器加载的,那么当然从应用程序类加载器开始加载,但是确确实实刚开始是多走了一次,但是这个类可能被加载很多次,下次再要使用的时候都是从应用程序类加载器加载,就不用向上委托了,说白了就是应用程序类加载器内部有个集合,第一次加载的时候放进去,然后下次使用的时候都在这个集合中去拿
这里边呢可能还有其它的作用,可能要站在jvm设计人员的角度去想。
说再多都没有用,还是看源码说话
来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader 的loadClass方法最终会调用其父类ClassLoader的loadClass方法,对应的就是下图我圈住的地方,该方法的大体逻辑如下:

首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接 返回。
如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加 载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加 载。
如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的 findClass方法来完成类加载。
AppClassLoader是Launcher类中的一个内部类,AppClassLoader.loadClass()它会调用super.loadClass(var1, var2);

但是这里有一个需要特别注意的点:
双亲委派机制并不是说 应用程序类加载器的父类是扩展类加载器,扩展类加载器的父类是引导类加载器,只不过是 应用程序类加载器的父加载器是扩展类加载器,扩展类加载器的父加载器是引导类加载器.应用程序类加载器以及扩展类加载器的父类都是URlClassLoader,和extClassLoader没有半毛钱关系,只不过应用程序类加载器的parent属性值是extClassLader。
AppClassLoader.loadClass()最终会调用到其父类ClassLoader的loadClass方法,如下图

这段代码中就实现了双亲委派机制
当前这个类加载器其实是AppClassLoader,只不过是调用了父类的classLoader方法,也就是说当前的this对象是AppClassLoader,刚刚双亲委派的时候讲过,如果这个类已经加载过就直接返回

这个方法就是在已经加载过的类中(刚刚说的集合)找对应的类,如果找到,直接返回,是不是符合我们的逻辑,这个findLoadedClass方法最终会调到C++的代码,所以也没必要往深入看这个方法了,接着往下看
第一次加载,c肯定等于null,所以继续往下走

这个时候,parent是不为空的,这个parent就是扩展类加载器,执行parent.loadClass()又会调用到ClassLoader的loadClass方法,就是又会进来当前这个方法(可以debug去看,这里就不详细展示了)
当前的类加载器是扩展类加载器,这个时候又执行findLoadedClass方法,肯定还是没有,这个时候又会进来

这个parent肯定是空的,因为扩展类加载器的父加载器是一个空的(前上一篇文章讲过,并且也有相关源码,这里就不再过多赘述),parent为空,那么肯定就进了else分支,也就是findBootstrapClassOrNull方法,BootstrapClass也就是引导类加载器,如果能加载到就返回,如果加载不到,当前就跳出了 ,当然findBootstrapClassOrNull底层也是调用了C++代码,这就是向上委托的逻辑。
由于第一次加载,那么引导类加载器也必然加载不到这个类,接着往下走

c==null是肯定的,因为引导类加载器没有加载到,然后会调用扩展类加载器的findClass方法,这个findClass方法是扩展类加载器继承的URLClassLoader的findClass方法,也就是执行的是URLClassLoader的findClass方法,但是当前的类加载器是扩展类加载器

这行代码主要就是生成了一个path,比如name是com.liuxs.jvm.Math,然后替换之后是com/liuxs/jvm/Math.class,然后ucp.getResource去target下面根据这个path找对应的class文件装载到jvm内存中,装载的过程就是defineClass方法
defineClass主要就是做的类加载的这几步:验证-准备-解析-初始化,如下图

但是现在肯定是加载不成功的,因为当前的类加载器是扩展类加载器,肯定是没有的,再回到ClassLoader的loadClass方法

所以这个c返回的肯定是空,因为它在嵌套调用,然后又会回到AppClassLoader,然后找到AppClassLoader的findClass方法,它的父类也是URLClassLoader,所以还是刚刚上面的那一段逻辑
生成了一个path,比如name是com.liuxs.jvm.Math,然后替换之后是com/liuxs/jvm/Math.class,然后ucp.getResource去target下面根据这个path找对应的class文件装载到jvm内存中,装载的过程就是defineClass方法.....
最后就会拿到这个类,这个就是双亲委派机制实现的源码,这个过程还是跟着我的思路动手debug一下比较好
为什么要设计双亲委派机制?
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性
看一个类加载示例
package java.lang;public class String {public static void main(String[] args) {System.out.println("**************My String Class**************");}}
自己定义了java.lang包,然后自己写了一个String类

经过上面的解析我们知道,java.lang.String.class会先被应用程序类加载器加载,然后经过扩展类加载器,然后到引导类加载器,但是引导类加载器会在jre\lib下面找有没有一个java.lang.String.class的类,它一找,发现有了,就会放到jvm中,然后返回,但是它返回的这个类是JDK自己定义的那个String类,所以运行之后就报错了

这个就是沙箱安全机制,说白了就是你想自己定义个包名等完全和JDK原本的类相同的类,然后去改逻辑是不可能的
还有一个就是避免类重复加载,比如说刚刚的String类,每个加载器中都有一份,但是最终只会加载一次
全盘负责委托机制
“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。
public class Math {public static int initData = 666;public static final int initDataFinal = 777;public static SysUser user = new SysUser();public int compute() { //一个方法对应一块栈帧内存区域int a = 1;int b = 2;return (a + b) * 10;}public static void main(String[] args) {Math math = new Math();math.compute();}}
比如上面这段代码,在加载Math这个类的时候避免不了加载SysUser这个类,因为它是一个静态的,这个SysUser类也是由AppClassLoader加载的,就不会使用其它的加载器加载
写在最后
如果您觉得这些文章对您有所启发和帮助,何不将它们与您的好友分享呢?这样,他们也能够享受其中的精彩内容,并从中获得启发。谢谢您的支持与分享!~
同时也希望您用发财的手帮忙点个关注,可以通过下方菜单点击福利领取上千套简历模板、几千道的面试题pdf以及几百G涵盖了Java开发,前端开发,小程序开发,数据库,测试等等的相关学习书籍与资料。

另外也可以通过点击交流群按钮添加我好友,然后拉您到自己的创建的Java知识分享群。一起去讨论、学习、成长、进步,谢谢~




