JVM 类加载
本文将介绍 JVM 中类加载机制的各种细节。
一、什么是类加载?
类文件中描述的信息,最终都需要加载到虚拟机中才能被使用。
所谓类加载,就是:将数据加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被 JVM 直接使用的类。
二、类的生命周期
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段,如图所示。
需要注意的是:
验证、准备、解析这三个阶段统称为连接
加载、验证、准备、初始化、卸载这五个阶段将会按顺序 开始,而解析阶段不一定
- 在某些情况下,解析阶段可以在初始化阶段之后再开始,这是为了支持 Java 的运行时绑定特性
- 是按顺序 开始 而不是按顺序 进行,是因为这些阶段通常是相互交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段
三、加载
1. 说明
在加载阶段,JVM 需要完成 3 件事情:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将二进制字节流(静态存储结构)转化为方法区的类型数据(运行时数据结构)
- 在内存(堆)中生成一个 Class 对象,程序将通过这一对象访问方法区中的类型数据
2. 加载的时机
《Java 虚拟机规范》中并没有强制规范,由虚拟机自行把握。
3. 类加载器
(1) 类加载器的两大作用
- 加载:类加载器负责完成加载阶段的工作,具体来说,类加载器会根据全限定名加载二进制字节流,转化生成 Class 对象
- 标识:类通过
类本身 + 类加载器
作为唯一标识
(2) 系统类加载器
在 JDK8 及以前,系统提供了以下三个类加载器:
启动类加载器 Bootstrap Class Loader:
- 是 JVM 的一部分
- 可能由 C++ 代码书写,也可能由 Java 代码书写,视 JVM 具体实现而定
- 负责加载
<Java_HOME>\lib 目录 + 被 -Xbootclasspath 参数所指定的目录
中的类库 - 无法在 Java 程序中访问
扩展类加载器 Extension Class Loader:
由 Java 代码书写
负责加载
<Java_HOME>\lib\ext 目录 + 被 java.ext.dirs 参数所指定的目录
中的类库JDK 允许用户将具有通用性的类库放置在 ext 目录中,以扩展 Java SE 的功能
可以在 Java 程序中访问,从而通过它加载 Class
应用程序类加载器 Application Class Loader:
- 又被称作系统类加载器
- 由 Java 代码书写
- 负责加载
ClassPath
中的类库 - 可以在 Java 程序中访问,访问方式为
ClassLoader.getSystemClassLoader()
,从而通过它加载 Class
(3) 自定义类加载器
用户也可以自定义自己的类加载器,从而实现从网络中获取类、运行时计算生成类等需求。
示例代码如下:
1 |
|
如果不想打破双亲委派机制,重写
findClass()
即可;如果希望打破双亲委派机制,应该重写loadClass()
。具体请看下文。
(4) 双亲委派机制
所谓双亲委派机制,其实就是 “叫父帮忙” 机制。
需要注意的是,
- “父子” 关系并不是通过继承实现,而是通过 “组合复用” 实现的
- 所谓 “父子” 关系,严格来说应该是层级关系
双亲委派机制具体为:
- 所有的加载器构成如上图所示的层级关系
- 如果有加载请求,应该优先递归交由上级加载,当上级无法加载时,才尝试自己加载。
ClassLoader 类的 loadClass()
方法中默认实现了双亲委派机制,具体代码如下:
1 |
|
双亲委派机制的优点是:
安全:通过双亲委派机制,可以保证 “基础类” 都由系统加载器加载,使得任意情况下通过任何类加载器加载所得的 “基础类” 都是相同的
而不是加载器 A 加载一份 String,加载器 B 加载一份 String。。。导致系统中一片混乱
避免重复加载:双亲委派机制可以保证类只被加载一次,避免重复加载
(5) 线程上下文类加载器
线程上下文类加载器与双亲委派机制
双亲委派机制是 Java 设计者的建议,而非强制规定,线程上下文类加载器便是对双亲委派机制的破坏。
为什么需要线程上下文加载器?
假设有类 C,C 中定义了一个接口,希望获取这个接口的实例。期望的具体做法是,提供一个方法让用户传入全限定名,在类 C 中加载类、实例化。
此时便会遇到问题,C 无法预知应该用什么类加载器加载,将导致类加载失败。
为此,JDK 提供了线程上下文加载器,可以将其作为传递类加载器的通道。
具体做法
获取并保存旧 ContextClassLoader:
ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader()
设置 ContextClassLoader:
Thread.currentThread().setContextClassLoader(contextClassLoader)
调用第三方方法,在第三方方法中,应该获取 ContextClassLoader 并使用
还原 ContextClassLoader:
Thread.currentThread().setContextClassLoader(oldContextClassLoader)
四、验证
对类的二进制字节流做一系列检查,确保其符合要求。
五、准备
为类的静态变量分配内存并赋初始值。
需要注意的是:
假设有静态变量
static int value = 123
,在此阶段结束后,value 的值应该是 0经编译后,
value = 123
将被放置于类构造方法<clinit>()
中,在类的初始化阶段才会被执行假设有静态变量
static final int value = 123
,在此阶段结束后,value 的值应该是 123经编译后,value 字段的属性表中将会有 ConstantValue 属性,并且其值为 123,准备阶段中便可以依据它设置初始值为 123
六、解析
1. 说明
将常量池内的符号引用替换为直接引用。
2. 解析的时机
《Java 虚拟机规范》并没有规定解析阶段发生的具体时间。因此,JVM 既可以在加载时就解析,也可以待被引用需要被使用时才解析。
3. 符号引用与直接引用
具体请看:
符号引用:一个字符串,用于描述被引用的目标
直接引用:reference,用于定位对象
4. 类或接口的解析
假设当前类为 D,需要解析将符号引用 N 解析为类或接口 C 的直接引用,
- 加载类:
- 如果 C 不是数组类型,将 N 代表的全限定名传递给 D 的类加载器加载
- 如果 C 是数组类型,首先按照第一步加载 C 的元素类型,接着由 JVM 生成 C
- 权限校验:确保 D 对 C 有的访问权限
5. 字段的解析
- 解析类:首先解析字段所在的类 C
- 搜索字段:
- 在 C 中搜索该字段,若找到,搜索结束
- 如果 C 实现了接口,按照继承关系递归搜索接口及其父接口,若找到,搜索结束
- 按照继承关系递归搜索父类,若找到,搜索结束
- 权限校验:确保具有对字段的访问权限
6. 方法的解析
- 解析类:首先解析方法所在的类或接口 C
- 检查:判断 C 是否是类
- 搜索方法:
- 在 C 中搜索该方法,若找到,搜索结束
- 按照继承关系递归搜索父类,若找到,搜索结束
- 如果 C 实现了接口,则按照继承关系递归搜索接口及其父接口,若找到,搜索结束,抛出
java.lang.AbstractMethodError
错误
- 权限校验:确保具有对方法的访问权限
因为方法的解析和接口方法的解析分开,所有需要检查方法是否是类方法
7. 接口方法的解析
- 解析类:首先解析方法所在的类或接口 C
- 检查:判断 C 是否是接口
- 搜索方法:
- 在 C 中搜索该方法,若找到,搜索结束
- 按照继承关系递归搜索父接口,若找到,搜索结束
- 如果 C 实现了接口,则按照继承关系递归搜索接口及其父接口,若找到,搜索结束
由于接口中的所有方法默认为 pubulic,因此不需要进行权限校验
七、初始化
1. 说明
执行程序员定义的初始化行为,即执行类构造方法 <clinit>()
。
2. 初始化的时机
于初始化阶段何时进行,《Java 虚拟机规范》进行了严格的规定,当且仅当 以下六种情况发生并且类型还未初始化时,必须进行初始化:
遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时
能生成这四条指令的 Java 代码有:
使用 new 关键字实例化对象
读取或设置一个类型的静态字段
被 final 修饰的除外,它将会被编译器优化为常量,直接获取到结果后放入常量池,因此实际上访问的是常量,而不是某个类型的某个静态字段
调用一个类型的静态方法
对类型进行反射调用时
类型的子类初始化时
接口定义了默认方法,其实现类初始化时
虚拟机启动时,指定该类型为要执行的主类
即执行其
main()
方法当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄时
关于动态语言支持,具体请看:
3. 类构造方法
- 类构造方法由
类变量初始化时赋值 + 静态语句块
构成 - 与构造方法不同,类构造方法中不需要显示父类的类构造方法,其调用将由 JVM 负责
- 类构造方法并不是必须的,如果没有类变量初始化赋值,也没有静态语句块,则类构造方法可以没有
参考
- 深入理解 Java 虚拟机