《Understanding the JVM》读书笔记之四——类加载机制
一、类的生命周期
其中,加载、验证、准备、初始化、卸载5个阶段都是顺序开始(但不一定是顺序结束)。而解析阶段则不一定,某些情况可以在初始化阶段之后再开始。
二、类加载过程
1. 加载
⚠️加载阶段与连接阶段的部分内容是交叉进行的
- 加载阶段,虚拟机需要完成3件事:
- 通过类的全名获取定义此类的二进制字节流;
- 将字节流中的二进制静态存储结构转化为方法区的运行时数据;
- 在内存中生成代表这个类的java.lang.Class对象,作为这个类的访问入口。
- 加载阶段完成后,二进制字节流会按JVM所需要的格式存储在方法区之中,在HotSpot虚拟机中,Class对象也存放在方法区。
- 对于非数组类,加载阶段可以通过系统提供的引导类加载器完成,也可以由用户自定义的类加载器完成,可以通过重写类加载器的loadClass()方法,来控制字节流获取方式。
- 对于数组类,数组本身通过JVM直接创建,不会通过类加载器来完成,但数组的元素类型时需要类加载器创建的。数组类的创建过程:
- 如果是引用类型数组:使用一般加载过程加载这个类型,该类型的加载器的类名称空间将标识出这是个数组;
- 如果不是引用类型数组,JVM把数组C标记为引导类加载器关联;
- 数组的可见性和组件类型的可见性一致,非引用类型的可见性为public
2. 验证
验证阶段不是不要的,可以通过参数-Xverify:none关闭大部分的验证措施
这是连接阶段的第一步,目的是确保Class文件的字节流信息符合当前虚拟机的要求,并且不会危及JVM自身的安全。主要包括4种不同的校验动作:a. 文件格式验证;b. 元数据验证;c. 字节码验证;d. 符号引用验证
-
文件格式验证
验证字节流是否符合Class文件格式的规范,并能被档期那版本的虚拟机处理。只有通过了这一阶段的验证字节流才会存储到方法区中,所以后边的3个验证全部都是基于方法区的存储结构进行的,不会再直接操作字节码。验证点包括:
a. 是否以魔数0xCAFEBABE开头
b. 主、次版本号是否在当前JVM处理范围之内
c. 常量池中的常量是否有不被支持的类型
d. …… -
元数据验证
对类的元数据进行验证,保证不存在不符合Java语言规范的元数据信息。主要验证点包括:
a. 此类是否有父类
b. 此类是否继承了final类
c. 若此类非抽象,是否实现了父类或父接口中要求实现的所有方法
d. …… -
字节码验证
对类的方法进行校验分析,保证被校验的类方法在运行时不会做出危害JVM安全的事件。验证点包括:
a. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
b. 保证跳转指令不会跳转到方法体以外的字节码指令上
c. ……
⚠️**jdk1.6对字节码校验进行了优化:**给方法体的Code属性的属性表中添加了一个“StackMapTable”属性,此属性描述了方法体中所有基本块开始时本地变量表和操作栈应有的状态,在字节码验证期间,只需要检查StackMapTable属性中的记录是否合法即可。 -
符号引用验证
对类自身意外的信息进行校验,通常的校验内容包括:
a. 符号引用中通过字符串描述的全限定名是否能找到对应的类
b. 指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
3. 准备
在这一阶段正式为类分配内存(在方法区中),并设置类变量的初始值。初始值一般情况下为0或null,静态常量会被设置为程序代码设定的值。
4. 解析
JVM将常量池内的符号引用替换为直接引用的过程
- 符号引用以一组符号来描述所引用的目标,与JVM的内存布局无关,符号引用的目标可以还不存在,符号引用的字面量形式在Java虚拟机规范中已经明确定义。
- 直接引用可以是直接指向目标的指针、相对偏移量、间接定位到的引用,和JVM的内存布局息息相关,如果时直接引用,目标必须在内存中存子。
解析动作主要针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号。
-
接口或类的解析
如果一个从未解析过的符号引用N被解析为一个类或接口C,则整个解析过程如下(假设当前代码所处位置在D类中):
a. 如果C不是数组类型:把N的全限定名传递给D的类加载器,去加载C,这时可能会触发其他类的加载动作。若此过程中发现异常,则解析失败。
b. 如果C时数组类型,且元素为引用类型,首先按a过程解析元素的类型,然后JVM生成一个代表此数组维度和元素的数组对象。
c. 如果上述步骤没有任何异常,会进行符号引用验证,确认D是否具备对C的访问权限,如果不具备访问权限,抛出IllegalAcessError异常。 -
字段的解析
字段的解析首先会对字段所属的类型或接口的符号引用进行解析,成功之后按一下步骤进行查找(字段所属的接口或类用C表示):
a. 如果C本身存在简单名称和字段描述都与目标相匹配的字段(属性),返回这个字段的直接引用,结束。
b. 若a没有,如果C实现了接口,按照继承关系从下往上递归搜索接口和它的父接口,如果接口中存在简单名称和字段描述与目标匹配的字段,返回,结束。
c. 如果b没有,且C不是Object类,按继承关系从下往上递归搜索父类,如果父类中存在简单名称和字段描述与目标匹配的字段,返回,结束。
d. 否则,查找失败,抛出NoSuchFieldEror异常。
e. 查找结束后,会对这个字段进行权限验证,如果发现不具备对这个字段的访问权限,抛出IllegalAccessError异常。 -
类方法解析
需要先解析类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,按以下步骤进行类方法搜索(所属的类用C表示):
a. 如果在类方法表中发现class_index索引的C是个接口,直接抛出IncompatibleClassChangeError异常。
b. 如果通过了a,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有,返回这个方法的直接引用,结束。
c. 否则,在父类中查找
d. 否则,在C实现的接口列表和他们的父接口列表递归查找,如果存在匹配的方法,说明C是一个抽象类,抛出AbstractMethodError异常。
e. 否则,宣告失败,抛出NoSuchMethodError异常。
f. 如果以上过程成功返回了直接引用,对这个方法进行权限验证。 -
接口方法解析
解析接口方法表的class_index项中索引的方法所属的类或接口类型符号引用,如果成功,按如下步骤进行搜索(C表示这个接口):
a. 如果发现C是个类而不是接口,直接抛出IncompatibleClassChangeError异常。
b. 若a通过,在C中查找是否存在简单名称和描述符都与目标想匹配的方法,有,返回这个方法的直接引用,查找结束。
c. 否则,在C的父接口中递归查找,知道Object类,查找……,有,返回,结束。
d. 否则宣告失败,抛出NoSuchMethodError异常。
e. 由于接口方法都时public的,所以不存在权限访问问题。
5. 初始化
此阶段开始真正执行类中定义的java代码
初始化阶段时执行类构造器的()方法的过程:
a. ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,静态语句块只能访问到定义到它之前的变量,之后的变量,静态语句块可以赋值,但不能访问。
b. ()方法不同于构造方法,不需要先调用父类的()方法,JVM会保证父类的()方法先执行。
c. 如果一个类中没有静态语句块,也没有变量的复制操作,编译器不会生产()方法。
d. 接口的()方法和父接口的()方法没有关系,也没有执行的先后顺序,接口的实现类的()方法执行时也不必先执行接口的()方法。
e. JVM会保证()方法的线程安全。
三、类初始化
虚拟机规范严格规定了类初始化的情况:(有且仅有5种):
- 遇到new、getstatic、putstatic、invokestatic四条指令时,如果没有进行过类初始化;(四条指令依次对应:new对象,获取、设置静态字段(非final),调用静态方法);
- 使用java.lang.reflect包中的方法进行反射调用时,如果类没有被初始化;
- 类初始化时,如果其父类没有被初始化,需要先初始化父类;
- 虚拟机启动时会先初始化main()方法所在的类;
- 动态语言支持:如果一个java.lang.invoke.MethodHandle实例的方法引用,这个方法所在的类没有被初始化。
**接口的初始化过程:**与类的初始化过程略有不同,主要表现在第3中情况。当一个接口要初始化时,并不需要它的父接口也进行过初始化,只有在父接口真正需要的时候才会被初始化。
博客园:https://www.cnblogs.com/logic-hatten/
所有内容同步更新,转载请私信或留言