Java学习记录--委派模型与类加载器
最近在读许令波的深入分析Java Web技术内幕
一书,对于学习Java以来一直有的几个疑惑得到了解答,遂记录下来.
我的疑问
- 双亲委派模型(实际上是一个翻译错误,英文为parent delegation,只是一个父委托模型)是什么?如何实现?为什么这样实现?
- 热加载的技术原理是什么?
- ClassLoader如何实现动态加载jar,实现插件模式系统?
下面跟着教程来寻找这些答案.
ClassLoader与委派模型
ClassLoader体系
ClassLoader
顾名思义是类加载器(准确来说为JVM平台类加载器抽象父类),主要功能负责将Class加载到JVM中,其所使用的加载策略叫做双亲委派模型.其主要有如下方法
- defineClass 负责把class文件从字节流解析为JVM能够识别的Class对象,这意味着只要能拿到对应的Class字节流就可以完成对象实例化,注意该Class对象在使用前必须resolve.(这种加载方式也是动态代理实现的基础,直接从内存中生成的class二进制流制造出来一个类)
- findClass 自定义规则时复写的方法,通常与defineClass一起使用,找到一个class文件,然后defineClass解析后生成Class对象,
- loadClass JVM所调用的加载方法,该方法会在findLoadedClass,loadClass(String)都没找到时调用findClass寻找Class对象,然后根据resolve的flag来决定是否链接.
- resolveClass 链接一个Class对象,在这个操作之后才可以使用该Class对象
JVM平台提供三个ClassLoader:
- Bootstrap ClassLoader,由C++实现的ClassLoader,不属于JVM平台,由JVM自己控制,主要加载JVM自己工作所需要的类,当类加载器的parent为null时会使用Bootstrap ClassLoader去加载,其也不再双亲委派模型中承担角色.
- ExtClassLoader,JVM在sun.misc.Launcher中主动实例化的类加载器,主要加载System.getProperty(“java.ext.dirs”)对应的目录下的文件(具体源码中可以看到),同时也是AppClassLoader的父类
- AppClassLoader,由ExtClassLoader为parent创建出来的,同样为sun.misc.Launcher的内部类,主要加载System.getProperty(“java.class.path”)下的类,这个目录就是我们经常用到的classpath,包括当前应用以及jre相关jar包路径.
那么不算Bootstrap ClassLoader
,JVM体系的ClassLoader结构如下
ClassLoader作为抽象父类其实是一装饰者模式中的Decorator角色,AppClassLoader本质上是对ExtClassLoader的增强处理,再看初始化方式可以简化为
1 | Launcher.AppClassLoader.getAppClassLoader(Launcher.ExtClassLoader.getExtClassLoader()) |
是不是和IO套用初始化很像?其实双亲委派按照我的理解本质上就是装饰者模式的应用,使用组合代替了继承只不过这个被装饰者叫做parent,思想上一致只是用法的不同.
另外这张图也说明如果想要实现自己的ClassLoader,只需要继承java.net.URLClassLoader
,然后自定义加载逻辑即可.
加载流程
加载
查找Class字节流,然后根据这个创建Java类的过程,这里要注意对于数组来说并没有对应的字节流,是由JVM直接生成,因此加载只适用于数组以外的Class文件流。
接下来重点看loadClass()
方法,该方法为加载class二进制文件的核心方法.
1 | protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException |
当父类不存在时调用java.lang.ClassLoader#findBootstrapClass
使用BootStrapClass加载,该方法是一个native方法,也进一步说明了Bootstrap Classloader
与JVM体系的ClassLoader没什么父子类关系.完全独立.
链接
链接的目的是把上述加载的类合并到JVM当中,使之可以运行,链接过程又分为验证 -> 准备 -> 解析
三个流程.验证
主要是确保加载的流符合JVM所定义的规范.准备
阶段主要是为静态字段分配内存,以及一些额外的预处理,解析
阶段主要是把编译期生成的符号引用变成转换为实际引用,所谓的符号引用是由于编译期无法确定其他类是否被加载,因此当该类使用外部类的字段或者方法时,是预先生成符号引用存放在Class文件的常量池中,如果对应符号引用的类还未被加载,那么会触发其加载流程,但不一定会触发其解析流程。
初始化
类加载过程的初始化主要针对静态字段,因为一个类只能被初始化一次,因此也确保了静态字段只被初始化一次,具体流程如下:
- final修饰的静态基本变量以及字符串:JVM会直接将其标记为常量值,直接完成初始化。
- 其他静态对象以及静态代码块:JVM会将其抽取到
<cinit>
方法中,然后执行该方法初始化静态字段。这是类的初始化流程。
另外JVM规定以下情况会触发类的初始化:
- 虚拟机启动时初始化用户的主类
- 使用new指令时,初始化对应的类
- 调用静态方法时,初始化静态方法所在的类
- 调用静态字段时,初始化静态字段所在的类
- 子类初始化会触发父类初始化
- 一个接口定义了default方法,那么直接或者间接实现该方法的类初始化会触发接口的初始化
- 使用反射API调用时,初始化这个类
- 初次调用MethodHandle时,初始化MethodHandle所在的类,这个是lambda延迟执行的原理。
类加载中异常
- ClassNotFoundException:一般是反射调用类,触发类加载时找不到相关的类抛出异常。
- NoClassDefFoundError:一般显示引用一个类,比如new关键词,但是类却加载不到导致的异常。一般是由于ClassNotFoundException类加载找不到但又显示引用了该类触发该异常。与ClassNotFoundException的区别就是是否显示引用了该类
提问解答
那么开始回答问题
1. 双亲委派模型是什么?
上述加载流程是 使用parent加载器加载类
-> parent不存在使用BootStrapClassLoader加载
-> 加载不到则使用子类的加载策略
,这里要注意BootStrapClassLoader
是由C++实现的JVM内部的加载工具,其没有对应的Java对象,因此不在这个委派体系中,类加载器本质上是装饰者模式组合思想的应用.
那么双亲是什么? 看ClassLoader的注释就能发现这只是个翻译问题parent->双亲,明明是单亲委派,装饰者模式是单类增强委托.
RednaxelaFX关于这点的证实
2. 委派模型如何实现?
委派模型从设计模式角度来看是一种组合设计,双亲委派这里更像是使用桥接模式实现的委托机制,由继承图可以发现ExtClassloader
与AppClassloader
处于同一层级,其内部又可以通过持有对应的private final ClassLoader parent
达到桥接委派的目的。
3. 为什么使用委派模型?
回答这个问题要先了解Java中是如何判定两个类是同一个类状况,如下段官方所说,也就是全类名(包括包名)相同并且他们的类加载器相同,那么两个对象才是等价的.
1 | At run time, several reference types with the same binary name may be loaded simultaneously by different class loaders. |
对于Object类因为父加载器先加载所以能保证对于所有Object的子类其所对应的Object都是由同一个ClassLoader所加载,也就保证了对象相等. 简单来说委托类优先模式保证了加载器的优先级问题,让优先级高的ClassLoader先加载,然后轮到优先级低的.
热加载的技术原理
热部署对于开发阶段的实用性极高,利用Jrebel等工具可以极大的节省应用调试时间.关于热加载技术可以参考文章http://www.hollischuang.com/archives/606,
对于一个被ClassLoader加载到内存的类来说,再次加载的时候就会被findLoadedClass()
方法所拦截,其判断该类已加载,则不会再次加载,那么热加载的技术本质是要替换到已加载的类.
对于Spring Boot devtools的restart技术,其是使用了两个ClassLoader,对于开发者所写的类使用自定义的ClassLoader,对于第三方包则使用默认加载器,那么每当代码有改动需要热加载时,丢弃自定义的ClassLoader所加载的类,然后重新使用其加载,如此做到了热部署.
对于Jrebel使用的貌似是修改类的字节码方式,具体不是很懂也就不讨论了.
对于Tomcat,其热部署技术是每次清理之前的引用,然后创建一个新的ClassLoaderWebClassLoader
来重新加载应用,这个加载使得永久代中对象增多,那么清理要求是full GC,这个是不可控的,所以也就导致了Tomcat热部署频繁会触发java.lang.OutOfMemoryErrorPermGen space
这个bug.
ClassLoader如何实现动态加载jar,实现插件模式系统?
ClassLoader的委派模型使得很容易扩展自定义的类加载器,那么基本步骤 定义自己的类加载器
-> 加载指定jar
-> 创建所需要的应用实例
,大概代码如下.
1 | String jarPath = "/Users/niuli/workspace/quding-git/quding-study/helloworld/target/hello-world-1.0-SNAPSHOT.jar"; |
另外插件模式的话一般还会有一些配置文件plugin.xml,告诉系统主要对外提供服务的类是什么以及一些默认配置等.不过大概思路都是大同小异.
另外既然有装载也就有卸载,卸载的必要条件是以下三个外,另外类是装载在永久代,那么卸载的触发也就是full GC才会去清理永久代中没有被强引用指向的类.
- 该类所有的实例都已经被GC。
- 加载该类的ClassLoader实例已经被GC。
- 该类的java.lang.Class对象没有在任何地方被引用。
补充题目
双亲委派模型中,从顶层到底层,都是哪些类加载器,分别加载哪些类?
Bootstrap ClassLoader: JVM自身需要的一些类,当Classloader中parent为null时执行该加载器尝试加载
ExtClassLoader: 主要加载java.ext.dirs
AppClassLoader; 主要加载java.class.path下的类,包括用户定义的类
纠正双亲委派模型,实际上就是装饰者模式应用.
双亲委派模型破坏举例
1.双亲委派模型是JDK1.2发布的模式,在这之前开发者是重写loadClass()这个方法实现自定义加载逻辑,该方法中又是双亲委派模型的关键算法,那么重写完全可以破坏该模型.
2.Java的SPI机制
1 | public static <S> ServiceLoader<S> load(Class<S> service) { |
SPI一些官方接口是定义在rt.jar中的,那么其所使用的是BootstrapClassloader,然而我们引入的第三方实现却是AppClassloader所管理,那么这里的问题就是BootstrapClassloader无法加载AppClassloader所管理的内容,也就是双亲委派无法逆序执行.那么想要逆序就需要破坏这一约束.利用Thread中上下文加载器来实现,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是AppClassloader
,那么在BootstrapClassloader
使用SPI加载时,则会利用线程上下文加载器委托AppClassloader
加载其实现类,那么这一过程与双亲委派相反,是破坏双亲委派原则的一种做法.
1 | try { |
3.热部署,热加载技术等都是破坏了双亲委派模型.
- 版权声明: 感谢您的阅读,本文由屈定's Blog版权所有。如若转载,请注明出处。
- 文章标题: Java学习记录--委派模型与类加载器
- 文章链接: https://mrdear.cn/posts/java_classloader.html