JVM架构之类加载系统,深入理解类加载器
JVM的整体架构
分为类加载,运行时数据区,执行引擎。这篇文章就先来讲讲类加载系统。
类加载系统
类的加载,是生成class对象的过程。将class
字节码信息加载进内存。类加载机制只负责class
文件的加载,至于是否可以执行,则是由执行引擎决定。
类加载流程(必须掌握)🚩
从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接。
1、加载
类加载过程的第一步,主要完成下面 3 件事情:
- 通过全限定名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
类的加载依赖于类加载器,后文会详细分析。
2、验证
检验二进制字节流是否符合JVM规范,比如魔数是否正确,版本号是否匹配等等。
3、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。只分配static修饰的变量的内存空间,且值为默认值,比如public static int value = 111
,此时value = 0,也就是赋予数据类型的默认值。
在 JDK 7 之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。
特例:public static final int value = 111
4、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:就是一组符号来描述目标,可以是任何字面量。
直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 符号引用:符号引用本身可以是任何形式的字面量,它的实现取决于JVM,只要能定位到目标即可。 符号引用的目标不一定被加载到内存中。
- 直接引用:直接指向目标的引用。直接引用的目标一定在内存中存在。
解析的时机
解析并不只出现在类加载过程,有些方法可能是真正被调用时才做解析操作。通常类加载时的解析包括了描述唯一版本的方法的符号引用,比如:final修饰的方法,版本唯一确定,就更适合在类加载阶段直接替换为直接引用。
《Java虚拟机规范》之中并未规定解析阶段发生的具体时间。只要求17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。比如:类加载时解析,或者符号引用将要被用才解析都是可以的。
对同一个符号引用进行多次解析请求是很常见的事情。除invokedynamic指令,其他解析请求的结果都可以被缓存。
invokedynamic:用于支持动态绑定
必须等到程序实际运行到这条指令时,解析动作才能进行。相对地,其余可触发解析的指令都是“静态”的
解析阶段是唯一一个顺序不确定的,在某些情况下解析可以在初始化阶段之后再开始,从而支持动态绑定。当然这里的类加载的顺序只是步骤开始的顺序,即可能存在一边加载,一边验证的情况,一个步骤的过程中激活了下一个步骤。
5、初始化
初始化阶段就是执行:类构造器 <clinit> ()
方法的过程,它是Javac编译器的自动生成物
主要是对static
修饰的字段进行赋值:
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
当然如果父类没有被初始化,会先初始化其父类。
注意只针对static修饰的类变量。
6、使用
有了class对象,就可以在堆上为这个类分配实例对象,或者调用它的静态方法了。
7、卸载
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
没有实例对象,没有被引用,且类加载器还要被GC,因此类的class对象被GC是一件不太发生的事件。
只有自定义类加载器加载的类才有可能被卸载,JVM三个自带的类加载器加载的Class永远不会被卸载。
类加载器🚩
类加载器只用于实现类的加载动作,但却用来标识类的唯一性。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等。
Bootstrap ClassLoader
引导类加载器,也被叫做启动类加载器或根类加载器。C++实现。
引导类加载器只为JVM提供加载服务,开发者不能直接使用它来加载自己的类。
负责加载:<JAVA_HOME>/lib目录下并且被虚拟机识别的类库
包括:java.util,java.io,java.lang下的类
Ext ClassLoader
源码在:sun.misc.Launcher$ExtClassLoader
加载<JAVA_HOME>\lib\ext
目录下
包括:以javax开头的类,swing系列、内置的js引擎、xml解析器等
App ClassLoader
源码在:sun.misc.Launcher$AppClassLoader
加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库
我们自己编写的代码以及使用的第三方的jar包都是由它来加载的。
类加载器的加载
Bootstrap会负责加载Extension,并将Extension的父加载器设置为Bootstrap自己。
Bootstrap随后加载AppClassLoader,并将AppClassLoader的父加载器设置为Extension。
AppClassLoader负责加载自定义类加载器,并将自定义ClassLoader的父加载器设置为AppClassLoader。
注意:在获取ExtClassLoader
的父类加载器时,获取到的结果为null,因为Bootstrap由C++实现。
Class类是按需加载的,使用到才触发加载。
命名空间:存储加载的类
每个类加载器都拥有一个自己的命名空间,用于存储被自身加载过的所有类的全限定名。子类加载器可以检查父类加载器中加载的类,通过拿类的全限定名在父类的命名空间内搜索匹配。但父类不可以看子类加载了哪些类。
JVM如何判断两个类是否相同
通过ClassLoaderId + PackageName + ClassName
进行判断。
这个非常重要,即类加载器的作用不仅仅有加载类,还用于区分类,为我们自定义类加载器的应用场景埋下伏笔。
双亲委派机制🚩
为什么需要双亲委派机制?
解决类加载器的隔离性问题,保证了 Java 程序的稳定运行
可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)
保证了 Java 的核心 API 不被篡改。
流程如下图:
总结:
- 自下而上,检查当前类加载器的命名空间内是否有这个类,即是否类已经被加载过。
- 自上而下,尝试加载类;自己加载不了这个类,就交给子类来加载。
如果加载失败,抛出ClassNotFoundException
异常
注意:类加载器的父子关系是优先级,而不是指java继承中的父子关系。
双亲委派的实现原理
除了Bootstrap其他的所有类加载器都继承自ClassLoader
ClassLoader.loadClass
方法,这个方法揭示了双亲委派机制:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 从自己的命名空间中查找该Class对象
Class<?> c = findLoadedClass(name);
if (c == null) {
// 自己的命名空间中没找到Class
try {
if (parent != null) {
// 父加载器 加载
c = parent.loadClass(name, false);
} else {
// Ext Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// findClass方法在ClassLoader中的处理是直接抛出ClassNotFoundException异常
// 是可扩展的一个方法 自定义处理
c = findClass(name);
}
}
// 是否需要解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
而ExtClassLoader没有重写loadClass()
方法,AppClassLoader加载器调用父类的loadClass()
方法,所以都没有打破双亲委派模型。
双亲委派的局限性
虽然避免了类的重复加载,但是当我们对类进行了修改,JVM是无法感知的,因为再次用到这个Class时,发现类加载器的命名空间内有了,就不会再次去尝试加载。此时我们必须重启整个项目。热部署就是解决这个问题的,而它的原理就是通过利用不同的类加载器,去加载更改后的class
文件,从而在内存中创建出两个不同的Class
对象。从而达到类文件更改后可以生效的目的。
自定义类加载器
一般继承ClassLoader,或是URLClassLoader ,重写方法即可。
如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
自定义类加载器的场景
类加载器实战
推荐李号双的《深入拆解Tomcat & Jetty》有详细分析Tomcat实现的类加载器,《深入理解Java虚拟机 第三版》也写的很好
参考文献
周志明 《深入理解Java虚拟机 第三版》
转载自:https://juejin.cn/post/7276698909782327352