虚拟机类加载机制
7.2、虚拟机类加载时机
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
加载,验证,准备,初始化,卸载是固定的。
什么时候加载,都可,虚拟机规范未制定。但是什么时候初始化,严格规定。
-
遇到 new getstatic putstatic invokestatic 这四条字节码指令,若类型未初始化,需要先出发其初始化阶段。
- 使用new实例化对象时。
- 读取或设置一个类型的静态字段。
- 调用一个类的静态方法时。
-
使用反射包进行反射调用时。
-
在初始化类时,若发现其父类未被初始化,则先初始化其父类。
-
虚拟机启动时,用户指定一个要执行的主类,main() 虚拟机优先初始化此类。
-
当接口中定义了jdk8的默认方法时,若接口实现类初始化,则接口在它之前被初始化。
在对一个类型进行主动引用时,这个类必须被初始化。
比如:
- 通过子类引用父类的静态字段,不会导致子类初始化。
- 通过数组定义来引用类,不会导致此类的初始化。
- 常量在编译期间存入调用类的常量池中,本质上未直接引用到定义常量的类,所以这个类不会被初始化。
为什么在初始化数组的时候数组元素的类未被初始化呢?
因为虚拟机自动创建的是一个由Java虚拟机自动生成的,直接继承于Object类的子类。创建动作由newarray触发
7.3、类加载的过程
7.3.1、加载
包括 加载,连接,连接包括验证 准备 解析,初始化。
加载阶段需要完成以下事情:
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将这个二进制字节流代表的静态存储结构转化为方法区运行时的数据结构。
- 在内存中生成一个代表该类的java.lang.Class 类型对象,作为方法区这个类的各种数据的访问入口。
Java虚拟机规范并未要求这个二进制字节流必须从某个Class文件中获取,所以我们可以从jar,war中读取。
数组不同,对于数组而言,数组本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的。
7.3.2、验证
确保Class文件字节流中的全部内容都符合Java虚拟机规范的全部约束。
分为,文件格式验证,元数据验证,字节码验证,符号引用验证。
7.3.3、准备
准备阶段是正式为类中定义的变量(类变量,静态变量)分配内存并设置初始值的阶段。这些变量的内存都应在方法区中分配。
jdk7以前,确实是在方法区永久代中分配的。但是在jdk7及以后,hotspot把字符串常量池及静态变量移出方法区,放入了堆里。而在jdk8及以后,类变量会随着Class对象一起存放在Java堆中,类变量在方法区就完全成为逻辑上的概念了。
强调
准备阶段,进行内存分配的仅仅是类变量,静态变量,而不包括实例变量。实例变量会随着对象的实例化随着对象一并分配到堆空间中。
public static int value = 123;
这段代码,在准备阶段完成后,初始值是0而不是123,因为这时并没有开始执行Java方法,而把value赋值的putstatic指令是程序编译后,存放于中,把value赋值为123要放在类的初始化阶段进行。
特殊情况
public static final int value = 123;
字段表中存在ConstantValue 我们在准备阶段就直接将123赋值给它了。
7.3.4、解析
将部分符号引用转化为直接引用。
符号引用与直接引用
符号引用:以一组符号描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可。引用的目标不一定是已经加载到虚拟机当中的内容。
直接引用:是可以直接指向目标指针、相对偏移量或者是一个能间接定位到目标的句柄。,有了直接引用,引用的目标必定已经在虚拟机内存中存在。
在同一实体中,如果一个符号引用之前已经被成功解析过,那么后续引用解析应一直成功。反之亦然。
解析动作主要针对:类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符。
1、类 或 接口的解析
假设,当前代码所处的类为D,如果要把一个从未解析的符号引用N解析为一个类或接口C的直接引用,那么虚拟机需要完成以下三个步骤。
- 如果C不是一个数组类型,那么虚拟机将会把代表N的全限定名传给D的类加载器,加载这个类C。加载是一个连锁反应,期间可能触发到多个接口的加载。只要有一个失败,就失败。
- 若C是一个数组类型,并且数组元素为对象,那么加载这个对象的类就像步骤一一样,然后由虚拟机在内存中直接创建一个代表该数组维度和元素的对象。
- 若上面两部已经完成, 那么C已经成为一个有效的类或接口了。但是我们还是要进行符号引用验证确认D是否具备访问C的权限。不具备权限,抛出IllegalAccessError异常。
针对以上三点,JDK9之后,我们还要检查模块间的访问权限。
2、字段解析
要解析一个未被解析过的字段符号引用,需要先对它所属的类或接口进行符号解析。若出现任何异常,则解析失败。
搜索步骤:
- 若C本身就包含了简单名称和仔段描述符都与目标匹配的字段,则直接返回这个字段的直接引用,查找结束。
- 若C中实现了接口,那么在它的各个接口和它的父接口,若出现简单名称和仔段描述符都与目标相同的字段,返回直接引用,查找结束。
- 否则,递归查找它的父类,找到结束。
- 否则,查找失败,报出NoSuchFieldError 异常。
若查找成功返回了字段的直接引用,则会对这个字段进行权限验证。若不具备对字段的访问权限,报IllegalAccessError
注意,类继承的类和多个接口中不能有同名同类型的实例变量。
3、方法解析
- 第一步和字段解析一样,需要先对它的父类和接口解析。
- 由于类和接口的方法符号引用的常量类型定义是分开的,若发现class_Index 索引类型是个接口的话,直接抛出IncompatiavleClassChangeError异常。
- 在类C的父类中递归查找是否有见到那名称和描述符相匹配的方法,错存在结束。
- 在类C父类查找。
- 在它们实现的接口中查找,若找到,则说明此类是个抽象类,抛出AbstractMethodError
- 否则,查找失败,报出NoSuchMethodError异常。
4、接口解析
先解析class_index属于的类或接口的符号引用,解析成功,是接口。
搜索:
- 若发现是类不是接口,会报出IncompatiavleClassChangeError异常。
- 在接口C中查找是否有简单名称和描述符匹配的方法,有找到。
- 否则,递归查找父接口,包括Object类。
- 失败。
注意,JDK9之后,有了接口静态私有方法,完全可能出现IllegalAccessError。
7.3.5、初始化
初始化阶段真正开始执行java类中编写的代码。将主导权交给程序。
初始化阶段,就是执行类构造器的阶段()方法的过程。
🔶方法是由编译器自动收集类中的静态变量的赋值动作以及静态语句块中的代码合并产生的。
编译器收集的顺序是由语句在源文件出现的顺序决定的。
注意,静态语句块只能访问到定义在静态语句块之前的变量,对于之后的变量,它可以赋值,但是无法访问。
未完…