欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

虚拟机内加载过程之解析和初始化阶段

程序员文章站 2022-05-27 23:46:31
...
解析阶段

解析阶段是虚拟机将常量池中的符号引用转换为直接引用的过程(在验证阶段我们知道,符号引用是将对类自身以外的信息进行匹配性验证,说人话就是说比如一个类A,调用了类B的方法,那么在解析阶段需要看下类A的中的符号能否定位到B类的方法).符号引用例如Constant_Class_info、Constant_Fieldref_info、Constant_Methodref_info 等.

符号引用:是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可. 符号引用的目标不一定要加载到内存中.
直接引用:是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄. 如果有了直接引用,那引用的目标必定存在于内存中.

虚拟机规范中并没有明确规定解析阶段发生的具体时间,只要求了在执行 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic 用于操作符号引用的字节码指令前,先对它们所使用的符号进行解析. 所以虚拟机实现可以根据需要来判断到底是在类被加载时就对常量池中的符号进行解析,还是等到一个符号将要被使用前才解析它.

除 invokedynamic 指令外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量表示为已解析状态)从而避免解析动作重复进行.

无论是否执行了多次解析,虚拟机需要保证在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一致成功,同样的,第一次解析失败,那么后续的解析应当收到相同的错误.

invokedynamic 指令比较特别,它是只有等到执行到这个指令的时候,才会触发解析动作.

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用.

类或接口的解析

比如说类D,要把一个符号引用N解析为一个类或接口C的直接引用,需要如下3个步骤:
1.如果C不是数组类型,那么虚拟机会将N传递给D的类加载器去加载类C,在加载过程中,由于元数据验证、字节码验证等操作可能会触发其他加载动作.
2.如果C是一个数组类型,并且数组的元素类型为对象,那么将会按照第一点的规则加载数据元素类型,如果不是对象,那么将会有虚拟机生成一个代表此数组维度和元素的数组对象.

有疑问。。。

3.如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,看D是否具备对C的访问权限.

字段解析

要解析一个未被解析过的字段符号引用,首先将会对字段表内 class_index 项中索引的 Constant_class_info 符号引用进行解析,也就是字段所属的类或接口的符号引用.如果在解析过程中出现异常,则解析失败,如果解析成功,那么将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索.

1.如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束.
2.否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束.
3.否则,如果C不是 java.lang.Object 的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束.
4.否则,查找失败,抛出 NoSuchFieldError 异常.

如果查找过程成返回了引用,则会对这个字段进行权限验证,如果不具备访问权限,则抛出异常.

类方法解析
和字段解析的第一个步骤一样.

1.类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现 class_index 中索引C是一个接口,那就直接抛出异常.
2.如果通过了第一步,在类C中查找是否具有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束.
3.否则,在类C的父类中递归查找是否有简单名和描述符都与目标相匹配的方法,如果有则返回直接引用,查找结束.
4.否则,在类C实现的接口列表及他们的父接口中递归查找是否有简单名和描述符都与目标相匹配的方法,如果存在说明类C是一个抽象类,说明类C是一个抽象类,查找结束,抛出异常.
5.否则,宣告方法查询失败,抛出 NoSuchMethodError.

最后,如果查找成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出异常.

接口方法解析

接口方法也需要先解析出接口方法表的 class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口.
1.与类方法解析不同,如果在接口方法表中发现 class_index 中的索引C是一个类而不是接口,那句直接抛出异常.
2.否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束.
3.否则,在接口C的父接口中递归查找,直到 java.lang.Object 类(包含Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束.
4.否则,宣告查找失败,抛出异常.

由于接口中所有方法默认都是 public 的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出 IllegalAccessError 异常.


初始化

初始化阶段才真正就是执行类中定义的 java 程序代码.

在初始化阶段,会根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>() 方法的过程.

<clinit>() 方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生,编译器收集的顺序是由语句在源程序中出现的顺序所决定的. 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.

<cllinit>() 方法和类的构造函数(<init>() 方法) 不同,它不需要显示调用父类构造器,虚拟机会保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行完毕. 因此在虚拟机中第一个被执行的 <clinit>() 方法肯定是 Object.

由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作.

<clinit>() 方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法.

接口中不能使用静态语句块,但任然有变量初始化的赋值操作,因此接口和类一样会生成 <clinit>() 方法,但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父类接口的 <clinit>() 方法. 只有当父接口中定义的变量使用时,父接口才会初始化. 另外,接口的实现类在初始化的时候也一样不会执行接口的 <clinit>() 方法.

虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其他线程都需要阻塞等待,直到活动线程执行完 <clinit>() 方法. 如果在一个类的 <clinit>() 方法中有很多耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往很隐蔽.