7.1类的加载过程和原理
目录介绍
- 01.什么是类加载
- 1.1 类加载概念
- 1.2 生命周期顺序图
- 02.类的加载
- 2.1 类加载过程
- 2.2 加载源
- 03.类的连接
- 3.1 验证
- 3.2 准备
- 3.3 解析
- 04.类的初始化
- 4.1 初始化说明
- 4.2 初始化案例测试
- 05.类的卸载
- 5.1 理解类卸载
- 07.看几个案例题
01.什么是类加载
1.1 类加载概念
- 什么是类加载
- 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制(懒加载)。
- 类加载过程
- 加载--连接(验证,准备,解析)--初始化--使用--卸载
- 下面介绍类加载每个阶段的任务:
- 加载(Loading):通过类的全限定名来获取定义此类的二进制字节流;将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义;在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口
- 验证(Verification):确保Class文件的字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证和符号引用验证
- 准备(Preparation):为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中;设置类变量初始值,通常情况下零值
- 解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程
- 初始化(Initialization):是类加载过程的最后一步,会开始真正执行类中定义的Java字节码。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制
1.2 生命周期顺序图
- 类的生命周期
- 类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
- 在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
- 另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
- 这7个阶段发生顺序如下图:
- 其中加载,验证,准备,解析及初始化是属于类加载机制中的步骤。注意此处的加载不等同于类加载。如何理解这句话?
02.类的加载
2.1 类加载过程
- 类的加载过程
- ①.通过一个类的全限定名来获取定义此类的二进制字节流
- ②.将这个字节流所代表的静态存储结构转换为方法区内的运行时数据结构
- ③.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.2 加载源
- 加载源
- (1)从本地系统直接加载
- (2)通过网络下载.class文件
- (3)从zip,jar等归档文件中加载.class文件
- (4)从专有数据库中提取.class文件
- (5)将Java源文件动态编译为.class文件(服务器)
03.类的连接
3.1 验证
- 验证是连接的第一步
- 这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证
- 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- (1)是否以魔数0xCAFEBABE开头。
- (2)主、次版本号是否在当前虚拟机处理范围之内。
- (3)常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
- (4)指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
- (5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
- (6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
- 元数据验证
- 对类的元数据信息进行语义校验,是否不存在不符合Java语言规范的元数据信息
- (1)这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)。
- (2)这个类是否继承了不允许被继承的类(被final修饰的类)。
- (3)如果这个类不是抽象类,是否实现了其父类或接口之中所要求实现的所有方法。
- (4)类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。
- 字节码验证
- 主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:
- (1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
- (2)保证跳转指令不会跳转到方法体以外的字节码指令上。
- (3)保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。
- 符号引用验证
- 符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:
- (1)符号引用中通过字符串描述的全限定名是否能够找到对应的类。
- (2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
- (3)符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
3.2 准备
- 准备
- 基本类型的属性在类加载后的准备阶段都会被初始化为默认值。为类变量分配内存并设置变量的初始值,这些变量使用的内存都将在方法区中进行分配。
- 整体类型是:
byte
,其值为8位有符号二进制补码整数,其默认值为零short
,其值为16位有符号二进制补码整数,其默认值为零int
,其值为32位有符号二进制补码整数,其默认值为零long
,其值为64位带符号的二进制补码整数,其默认值为零char
,其值为16位无符号整数,表示基本多语言平面中的Unicode代码点,使用UTF-16编码,其默认值为空代码点('\u0000'
)
- 浮点类型是:
float
,其值是浮点值集的元素,或者,如果支持,则为float-extended-exponent值集,其默认值为正零double
,其值是double值集的元素,或者,如果支持,则为double-extended-exponent值集,其默认值为正零- 所述的值
boolean
类型编码的真值true
和false
,并且缺省值是false
。
- 参考类型和值
- 有三种
reference
类型:类类型,数组类型和接口类型。它们的值分别是对动态创建的类实例,数组或类实例或实现接口的数组的引用。 - 数组类型由具有单个维度的组件类型(其长度不是由类型给出)组成。数组类型的组件类型本身可以是数组类型。如果从任何数组类型开始,考虑其组件类型,然后(如果它也是数组类型)该类型的组件类型,依此类推,最终必须达到不是数组类型的组件类型;这称为数组类型的元素类型。数组类型的元素类型必须是基本类型,类类型或接口类型。
reference
值也可以是专用空引用的,没有对象的引用,这将在这里通过来表示null
。该null
引用最初没有运行时类型,但可以转换为任何类型。reference
类型的默认值是null
。
- 有三种
3.3 解析
- 解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。
- 解析对象包括:
- 类或者接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
- 类或者接口的解析
- 假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的引用,那虚拟机完成整个解析过程需要以下3个步骤:
- (1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。
- (2)如果C是一个数组类型,并且数组的元素类型为对象,那将会按照第1点的规则加载数组元素类型。
- (3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具有对C的访问权限。如果发现不具备访问权限,则抛出java.lang.IllegalAccessError异常。
- 字段解析
- 首先解析字段表内class_index项中索引的CONSTANT_Class_info符号引用,也就是字段所属的类或接口的符号引用,如果解析完成,将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。
- (1)如果C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
- (2)否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
- (3)否则,如果C 不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
- (4)否则,查找失败,抛出java.lang.NoSuchFieldError异常。
- 类方法解析
- 首先解析类方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个类方法所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续类方法的搜索。
- (1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C 是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
- (2)如果通过了第一步,在类C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
- (3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
- (4)否则,在类C实现的接口列表以及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在相匹配的方法,说明类C是一个抽象类这时查找结束,抛出java.lang.AbstractMethodError异常。
- (5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
- 接口方法解析
- 首先解析接口方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个接口方法所属的接口用C表示,虚拟机规范要求按照如下步骤对C进行后续接口方法的搜索。
- (1)与类解析方法不同,如果在接口方法表中发现class_index中的索引C是个类而不是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
- (2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
- (3)否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
- (4)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
04.类的初始化
4.1 初始化说明
- 什么时候初始化
- 遇到
new
、getstatic
、putstatic
或invokestatic
这四个字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
- 遇到
- 生成这四条命令的最常见的静态字段
- 使用new关键字实例化对象的时候,这种最常见。
- 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
- 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
- 使用反射对类进行调用,如果该类没有进行初始化 ,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要制定一个需要执行的主类,即:包含main方法的类。虚拟机会先初始化这个类。
- 当使用JDK1.7的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出发初始化。
- 不能被初始化的例子
- 通过子类引用父类的静态字段,子类不会被初始化
- 通过数组定义来引用类
- 调用类的常量(常量在编译阶段就存入调用类的常量池中了)
4.2 初始化案例测试
- 示例1:
public class Fantj { public static int high = 180; static { System.out.println("静态初始化类Fantj "); high = 185; } public Fantj(){ System.out.println("创建Fantj 类的对象"); } } public class Main { public static void main(String[] args) { Fantj fantj = new Fantj(); System.out.println(fantj.high); } }
- 控制台打印:
静态初始化类Fantj 创建Fantj 类的对象 185
- 控制台打印:
- 该代码案例说明
- 1.jvm加载Main类,首先在方法区生成Main类对应的
静态变量
、静态方法
、常量池
、代码
等,同时在堆里生成Class对象(反射对象),通过该对象可以访问方法区信息,类Fantj
也是如此。 - 2.
main
方法执行,一个方法对应一个栈帧,所以Fantj
压栈,一开始fantj
是空,Fantj
压栈的同时堆中生成Fantj
对象,然后把对象地址交付给fantj
,此时fantj
就拥有了Fantj
对象地址。 - 3.
fantj.high
来调用方法区的数据。
- 1.jvm加载Main类,首先在方法区生成Main类对应的
- 好了,试试静态方法。给Fantj类加个方法:
public static void boss(){ System.out.println("boss静态方法初始化"); } public class Main { public static void main(String[] args) { // Fantj fantj = new Fantj(); // System.out.println(fantj.high); Fantj.boss(); } }
- 打印结果
静态初始化类Fantj boss静态方法初始化
- 说明了调用静态方法没有对类进行实例化,所以静态类加载会被初始化。
- 打印结果
05.类的卸载
5.1 理解类卸载
- 如何理解类卸载
- 由Java虚拟机自带的三种类加载器,加载类,在虚拟机整个的生命周期中都不会被卸载,只有用户自定义的类加载器所加载的类,才可以被卸载。
07.看几个案例题
7.1 常量变化的原理
看下面这段代码,说一下准备阶段和初始化阶段常量变化的原理?
public static int value1 = 5; public static int value2 = 6; static{ value2 = 66; }
准备阶段和初始化阶段常量变化?
- 在准备阶段
value1
和value2
都等于0; - 在初始化阶段
value1
和value2
分别等于5和66;
- 在准备阶段
变量初始化过程
- 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是
<clinit>
方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用; - 编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;
- 如果超类还没有被初始化,那么优先对超类初始化,但在
<clinit>
方法内部不会显示调用超类的<clinit>
方法,由JVM负责保证一个类的<clinit>
方法执行之前,它的超类<clinit>
方法已经被执行。 - JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用静态内部类实现线程安全的单例模式)
- 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类
<clinit>
方法;
- 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是
Java虚拟机是如何加载Java类的?
- https://time.geekbang.org/column/article/11523
请介绍类加载过程,什么是双亲委派模型?
- https://time.geekbang.org/column/article/9946
深入拆解Java虚拟机(二)Java虚拟机是如何加载Java类的?
- https://www.cnblogs.com/SmartCat994/p/14252262.html
JVM实战:类加载机制+JVM调优实战+代码优化
- https://www.jianshu.com/p/da4dc3202ff7