Java类加载机制&类加载器学习笔记
Java文件执行过程:
Java文件通过javac编译成.class文件(字节码文件)。JVM加载字节码文件,运行时解释器将字节码解释为机器码来执行。在整个程序运行期间,即时编译器会针对热点代码将该部分字节码编译为机器码以获得更高得执行效率。在运行期间解释器和即时编译器相互配合,使Java程序几乎可以达到和编译型语言一样的执行速度。
类加载机制
JVM加载字节码文件的过程,即为类加载。
目的:把一份被javac编译的class文件,通过加载生成某种形式的class数据结构进入内存,程序调用这个加载进来的数据结构来构造出对应的Object对象。
Java类生命周期
Javac编译---->加载---->连接:验证---->连接:准备---->连接:解析---->初始化---->使用---->卸载
- 只有加载步骤中的读取二进制流与初始化部分,能够被上层开发者控制,而剩下的所有步骤,都是有JVM掌控的,其中细节由JVM的开发人员处理,对上层开发者来说是个黑盒。
- 加载步骤JVM只提供了一个拓展接口,用于class文件二进制流的读取。上层开发者使用这个接口实现了动态代理、热部署等功能。
- 解析步骤是灵活的,可以在初始化环节之前或之后进行,实现后期绑定,其他环节的顺序不可改变。
类加载
其中类加载只包含类的生命周期中的五个阶段:
加载---->连接:验证---->连接:准备---->连接:解析---->初始化
加载
作用:读取class文件(不止本地class文件,泛指各种来源的二进制流),将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户使用的java.lang.class类型的对象(类对象)。(动态代理技术就是使用的即时计算的class文件,然后实例化代理对象)
连接-验证
验证阶段有很多步骤来进行验证,并且分散在各个不同的阶段内。且验证的内容会不断发展,之后会出现各种各样其他的验证机制。
- 检验文件格式:在加载阶段进行,若通过才能顺利记载进内存。
- 方法区内元数据验证&字节码验证:发生在验证阶段,主要是对class静态结构进行语法和语义上的分析保证其不会产生危害JVM的行为。如果这两个步骤验证通过,JVM会暂且认为该class是安全的。但并不是说验证阶段完全结束。
- 符号引用验证:在解析阶段验证。
连接-准备
准备阶段是为该类型中定义的静态变量赋0值。
Class A {
private int a = 1;
static private int b; // 准备阶段是为静态变量赋0值
static {
b = 1;
}
}
连接-解析
解析阶段:将符号引用替换为直接引用。
- 符号引用和直接引用:当一个java类被编译成class 之后,假设这个类为A,A中引用了B,在编译阶段A并不知晓B有没有被编译,而且此时B也一定没有被加载,所以A肯定不知道B的实际地址。此时A的class文件会使用一个字符串S来代表B的地址,S就被称为是符号引用。
运行时,若果A发生了类加载到解析阶段发现B还未被加载,将会触发B的类加载,将B加载到JVM中,此时A中B的符号引用将会被替换为B的实际地址,即直接引用。 - 多态的实现:Java通过后期绑定的方式来实现多态。后期绑定的实现就是动态解析。
A引用的B,B可能是抽象类或者是接口,B会有多个具体的实现类,此时B的具体实现并不明确,不能确定是用哪个实现类的直接引用来进行替换。等到运行过程中虚拟机调用栈会得到具体的类型信息,这时候在进行解析,就能确定具体的实现类的直接引用来替换符号引用。
这种情况解析阶段会发生在初始化阶段之后,这就是动态解析,用来实现了后期绑定。 - 当解析步骤完成说明整个连接部分的完成,意味着外部加载的Java类已经成功引入到了程序中。
初始化
初始化阶段: 此时会判断代码中是否存在主动的资源初始化动作,如果有就执行。
主动资源初始化: 不是指类的构造函数,而是class层面的。成员变量的赋值、静态变量的赋值、静态代码快的逻辑。只有显示的调用new指令,才会调用构造函数进行对象的实例化。
Class A {
private int a = 1; // 初始化阶段是为成员变量赋值
static private int b;
static {
b = 1; // 初始化阶段 静态代码块逻辑
}
}
Java类加载器
类加载器的分类
类加载器是抽象概念,各个不同的JVM实现方式不尽相同。JVM规范中类加载器分为两大类:
- Bootstrap ClassLoader :启动类加载器
- other:非启动类加载器
Hotspot JVM具体实现
启动类加载器
Bootstrap ClassLoader: 采用c/c++实现,嵌套在JVM内部无法作为对象被程序引用,主要用来加载Java核心类库。比如<JAVA_HOME>/lib 路径下的jar包,由启动参数指定路径下的核心类库等。
非启动类加载器
Hotspot JVM 实现的非启动类加载器有三类,他们都使用Java实现,都继承自java.lang.Classloader,他们可以作为对象被引用。
- Extension ClassLoader :主要用来加载<JAVA_HOME>/lib/ext目录下,或者是由系统变量指定得路径中的类库。它希望加载的是JavaAPI的扩展,是对Java类库的补充能力。
- Application ClassLoader:主要用来加载环境变量classpath/java.lang.path,或系统属性指定路径下的类库。它希望加载的是上层程序员编写的代码,以及第三方类库。平时编写的代码几乎都是由Application ClassLoader 来进行加载。
- User ClassLoader:用户自己编写的类加载器。前两个类加载器都只能从本地文件中获取字节码来进行加载,User ClassLoader可以让用户能够获取任何来源的字节码,并加载。
双亲委派模型
在默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在程序中,他是唯一的,不会产生歧义。
- 双亲委派:在被动的情况下,当一个类加载器收到加载请求时,首先他不会自己去加载,而是传递给父亲加载器, 这样所有的类都会传递给最上层的加载器,只有最上层的加载器无法完成加载(根据类的限定名类加载器无法在自己负责的路径中找到该类),儿子加载器才会尝试加载。
- 父亲加载器和儿子加载器并不是父类加载器和子类加载器,父亲加载器 这些并不表示继承关系,而是一种逻辑关系,实际是通过组合的方式实现。
结论: - 除了Bootstrap ClassLoader,所有的非Bootstrap ClassLoader 都继承了java.lang.ClassLoader,都由这个类的defineClass进行后续处理。
- 越核心的类库被越上层的类加载器加载,而某限定名的类一旦被加载过了,被动情况下,就不会再加载相同限定名的类,这样就能有效避免加载混乱。
- 双亲委派模型并不是一个具有强约束力的模型,史上存在三次对双亲委派模型的破坏。
视频链接:https://www.bilibili.com/video/BV14U4y1L75q?spm_id_from=333.999.0.0