3.准备

准备阶段是正式为类的静态成员变量分配内存,并设置初始值(初始值“通常情况”下是数据类型的零值)的阶段。
这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
假设一个类变量的定义为:

  1. public static int value = 123;

变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。
基本数据类型的零值:

数据类型 零值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

上面提到在“通常情况”下初始值是零值,那言外之意是相对的会有某些“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,假设上面类变量value的定义修改为:

  1. public static final int value = 123;

编译后字节码:

  1. Classfile /D:/Ready.class
  2. Last modified 2021-8-22; size 323 bytes
  3. MD5 checksum 1b197915650a61d33c2a9ff639d87484
  4. Compiled from "Ready.java"
  5. public class jvm.classloading.Ready
  6. minor version: 0
  7. major version: 52
  8. flags: ACC_PUBLIC, ACC_SUPER
  9. Constant pool:
  10. #1 = Methodref #3.#17 // java/lang/Object."<init>":()V
  11. #2 = Class #18 // jvm/classloading/Ready
  12. #3 = Class #19 // java/lang/Object
  13. #4 = Utf8 value
  14. #5 = Utf8 I
  15. #6 = Utf8 ConstantValue
  16. #7 = Integer 123
  17. #8 = Utf8 <init>
  18. #9 = Utf8 ()V
  19. #10 = Utf8 Code
  20. #11 = Utf8 LineNumberTable
  21. #12 = Utf8 LocalVariableTable
  22. #13 = Utf8 this
  23. #14 = Utf8 Ljvm/classloading/Ready;
  24. #15 = Utf8 SourceFile
  25. #16 = Utf8 Ready.java
  26. #17 = NameAndType #8:#9 // "<init>":()V
  27. #18 = Utf8 jvm/classloading/Ready
  28. #19 = Utf8 java/lang/Object
  29. {
  30. public static final int value;
  31. descriptor: I
  32. flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  33. ConstantValue: int 123
  34. public jvm.classloading.Ready();
  35. descriptor: ()V
  36. flags: ACC_PUBLIC
  37. Code:
  38. stack=1, locals=1, args_size=1
  39. 0: aload_0
  40. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  41. 4: return
  42. LineNumberTable:
  43. line 3: 0
  44. LocalVariableTable:
  45. Start Length Slot Name Signature
  46. 0 5 0 this Ljvm/classloading/Ready;
  47. }
  48. SourceFile: "Ready.java"

可以看出Javac将会为value生成ConstantValue属性,在准备阶段时虚拟机就会根据Con-stantValue的设置将value赋值为123

4.解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

  • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中
  • 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

《Java虚拟机规范》并未规定解析阶段发生的具体时间,只要求了在执行ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
对方法或者字段的访问,也会在解析阶段中对它们的可访问性(public、protected、private、)进行检查。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行,分别对应于常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8种常量类型。

4.1 类或接口的解析

假设将类D的一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要以下3个步骤:

  1. 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如,加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就将宣告失败。
  2. 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第一点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表该数组维度和元素的数组对象。
  3. 如果上面两步没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。

    4.2 字段解析

    要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index(CONSTANT_Fieldref_info)项中索引的符号引用进行解析,也就是字段所属的类或接口的符号引用。
    如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那把这个字段所属的类或接口用C表示。
    《Java虚拟机规范》要求按照如下步骤对C进行后续字段的搜索:

  4. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

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

如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
以上解析规则能够确保Java虚拟机获得字段唯一的解析结果,但在实际情况中,Javac编译器往往会采取比上述规范更加严格一些的约束,譬如有一个同名字段同时出现在某个类的接口和父类当中,或者同时在自己或父类的多个接口中出现,按照解析规则仍是可以确定唯一的访问字段,但Javac编译器就可能直接拒绝其编译为Class文件。

  1. public class FieldResolution {
  2. interface Interface0{
  3. int A = 0;
  4. }
  5. interface Interface1 extends Interface0{
  6. int A =1;
  7. }
  8. interface Interface2{
  9. int A =2;
  10. }
  11. static class Parent implements Interface1{
  12. public static int A =3;
  13. }
  14. static class Sub extends Parent implements Interface2{
  15. public static int A =4;
  16. }
  17. public static void main(String[] args) {
  18. System.out.println(Sub.A);
  19. }
  20. }

在上面代码中演示了这种情况,如果注释了Sub类中的“public static int A=4;”,接口与父类同时存在字段A,那Oracle公司实现的Javac编译器将提示“Thefield Sub.A is ambiguous”,并且会拒绝编译这段代码。

4.3 方法解析

方法解析的第一个步骤与字段解析一样,也是需要先解析出方法表的class_index(CONSTANT_Methodref_info)项中索引的方法所属的类或接口的符号引用,如果解析成功,那么我们依然用C表示这个类。
接下来虚拟机将会按照如下步骤进行后续的方法搜索:

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

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

4.4 接口方法解析

接口方法也是需要先解析出接口方法表的class_index (CONSTANT_InterfaceMethodref_info)项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口。
接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:

  1. 与类的方法解析相反,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那么就直接抛出java.lang.IncompatibleClassChangeError异常。
  2. 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  3. 否则,在接口C的父接口中递归查找,直到java.lang.Object类(接口方法的查找范围也会包括Object类中的方法)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  4. 对于规则3,由于Java的接口允许多重继承,如果C的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法中返回其中一个并结束查找,《Java虚拟机规范》中并没有进一步规则约束应该返回哪一个接口方法。但与之前字段查找类似地,不同发行商实现的Javac编译器有可能会按照更严格的约束拒绝编译这种代码来避免不确定性。
  5. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

在JDK 9之前,Java接口中的所有方法都默认是public的,也没有模块化的访问约束,所以不存在访问权限的问题,接口方法的符号解析就不可能抛出java.lang.IllegalAccessError异常。但在JDK 9中增加了接口的静态私有方法,也有了模块化的访问约束,所以从JDK 9起,接口方法的访问也完全有可能因访问权限控制而出现java.lang.IllegalAccessError异常