翻译自Chapter 5. Loading, Linking, and Initializing

Java 虚拟机动态的加载,连接,初始化类或者接口。

加载是一个通过特殊符号查找类或者接口类型的二进制文件,同时使用二进制文件创建类或者接口的过程。

连接是一个加载类或者接口并结合它变为Java虚拟机的运行时状态的过程,以便于它可以被Java虚拟机执行。

一个类或者接口的初始化由执行类或者接口的初始化方法<clinit>组成(§2.9)。

下图是类或者接口动态加载、连接、初始化的过程:

jvm-classloading

图片摘自深入理解Java虚拟机

1. 加载

加载阶段:

    1. 通过一个类的全限定名来获取定义此类二进制字节流。
    1. 将这个字节流代表的静态存储结构转换为方法区的运行时数据结构(运行时数据结构详见第四小节)。
    1. 内存(Class对象比较特殊,它虽然是对象,但是存储在方法区中)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

对于数组类本身不通过类加载器创建,它由Java虚拟机直接创建。数组类型却由类加载器创建,创建过程遵循以下规则:

  • 如果数组组件类型是引用类型,则数组被标记为组件类型定义的类加载器定义。否则,数组被标记为引导类加载器定义。
  • 如果数组的组件类型不是引用类型,Java虚拟机将会把数组标记为与引导类加载器关联。
  • 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性默认是public。

加载和连接阶段是交叉进行的。

2. 连接

如果需要连接类或接口涉及验证和准备该类或接口,直接超类,直接超接口及其元素类型(如果它是数组类型)。类或接口中符号引用的解析是连接的可选部分。

只要维护了以下所有属性,此规范允许实现灵活性,以便何时发生连接活动(以及由于递归,加载)。

  • 类或接口在连接之前已完全加载。

  • 在初始化之前,类或接口已完全验证并准备好。

在连接期间检测到的错误被抛出到程序中的某个点,程序可能会直接或间接地需要连接到错误中涉及的类或接口。

例如,Java虚拟机实现可以选择在使用它时分别解析类或接口中的每个符号引用,或者在验证类时立即解析它们。这意味着在一些实现中,在初始化类或接口之后,解析过程可以继续。无论采用哪种策略,在解析期间检测到的任何错误都必须抛出到程序中(直接或间接)使用对类或接口的符号引用的位置。

因为连接涉及新数据结构的分配,所以它可能会失败OutOfMemoryError。

2.1 验证

验证阶段会完成4个阶段的验证动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

2.2 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。进行内存分配的仅包括类变量[static变量],不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

2.3 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, putstatic,这些虚拟机指令对运行时常量池进行符号引用,执行任何这些指令都需要解析其符号引用。

解析是从运行时常量池中的符号引用动态确定具体值的过程。

对出现在invokedynamic指令的相同的符号引用被解析一次并不意味着被任何其他invokedynamic指令认为已解析。

对于上述提到的所有指令,如果其中一个指令对符号引用进行了解析,则意味着任何非invokedynamic指令认为这个符号引用已经解析。

如果在解析符号引用期间发生错误,则必须在程序中(直接或间接)使用符号引用时的某一点抛出IncompatibleClassChangeError(或子类)的实例。

如果Java虚拟机尝试解析符号引用失败,抛出的错误是LinkageError(或子类)的实例,后续尝试解析引用始终失败,并且和初始解析尝试而引发的错误相同。

在执行指令之前,不得解析特定invokedynamic指令对调用site说明符的符号引用。

invokedynamic指令解析失败的情况下,后续解析尝试不会重新执行引导方法。

上述某些指令在解析符号引用时需要额外的连接检查。例如,为了使getfield指令成功解析对其运行的字段的符号引用,它不仅必须完成第5.4.3.2节中给出的字段解析步骤,还要检查字段是否为静态。如果它是静态字段,则必须抛出链接异常。

值得注意的是,为了使invokedynamic指令成功解析对调用site说明符的符号引用,其中指定的引导方法必须正常完成并返回合适的调用站点对象。如果引导方法突然完成或返回不合适的调用站点对象,则必须抛出连接异常。

连接由特定执行特定Java虚拟机指令检查生成的异常在该指令的描述中给出,并且在本解析的一般性讨论中未涉及。请注意,此类异常虽然被描述为Java虚拟机指令执行而非解析的一部分,但仍然被视为解析失败。

3. 初始化

一个类的加载过程中加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,解析阶段则不一定。解析可以在初始化完成后再开始,这时为了支持Java的运行时绑定。

Java虚拟机没有进行强制约束什么时候加载,只是严格规范了5中情况必须对类进行”初始化”。

  • 遇到new、getstatic、putstatic或invokestatic这4条指令时,如果类没有进行初始化,则需要先触发其初始化。4条指令的常见场景是:使用new 关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)、以及调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用JDK1.7及以上版本的动态语言(详细了解 Java动态语言支持 –周志明)支持时,如果一个java.lang.incoke.MethodHandle实例最后解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则先触发其初始化。

初始化阶段是执行类构造器<clinit>()方法的过程。

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

1
2
3
4
5
6
7
8

public class Test(){
static {
i = 0;
System.out.print(i);
}
static int i = 1;
}

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()不需要执行其父接口的<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待。

4. 运行时常量池

Java虚拟机维护每种类型常量池,这是一种运行时数据结构,它服务于常规编程语言实现的符号表的许多目的。

类或接口的二进制表示形式中的constant_pool表用于在创建类或接口对象时构造运行时常量池。运行时常量池中的所有引用最初都是符号引用。运行时常量池中的符号引用是从类或接口的二进制表示中的结构派生的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 字符串
String str = "str";

System.out.println(str);

// 基本类型
int i = 1;

// 基本类型数组
int[] arrayI = new int[3];

// 引用类型数组
A [] arrayA = new A[3];

// 引用类型
A a = new A();

// 引用方法
a.test();

// 接口声明
C c = new B();

// 接口方法
c.test();

// lambda
Runnable x = ()->{};

javap -v编译如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
Constant pool:
#1 = Methodref #13.#42 // java/lang/Object."<init>":()V
#2 = String #25 // str
#3 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #45.#46 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #47 // com/zhongyp/test/A
#6 = Methodref #5.#42 // com/zhongyp/test/A."<init>":()V
#7 = Methodref #5.#48 // com/zhongyp/test/A.test:()V
#8 = Class #49 // com/zhongyp/test/B
#9 = Methodref #8.#42 // com/zhongyp/test/B."<init>":()V
#10 = InterfaceMethodref #50.#48 // com/zhongyp/test/C.test:()V
#11 = InvokeDynamic #0:#55 // #0:run:()Ljava/lang/Runnable;
#12 = Class #56 // com/zhongyp/test/Test
#13 = Class #57 // java/lang/Object
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lcom/zhongyp/test/Test;
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Utf8 args
#24 = Utf8 [Ljava/lang/String;
#25 = Utf8 str
#26 = Utf8 Ljava/lang/String;
#27 = Utf8 i
#28 = Utf8 I
#29 = Utf8 arrayI
#30 = Utf8 [I
#31 = Utf8 arrayA
#32 = Utf8 [Lcom/zhongyp/test/A;
#33 = Utf8 a
#34 = Utf8 Lcom/zhongyp/test/A;
#35 = Utf8 c
#36 = Utf8 Lcom/zhongyp/test/C;
#37 = Utf8 x
#38 = Utf8 Ljava/lang/Runnable;
#39 = Utf8 lambda$main$0
#40 = Utf8 SourceFile
#41 = Utf8 Test.java
#42 = NameAndType #14:#15 // "<init>":()V
#43 = Class #58 // java/lang/System
#44 = NameAndType #59:#60 // out:Ljava/io/PrintStream;
#45 = Class #61 // java/io/PrintStream
#46 = NameAndType #62:#63 // println:(Ljava/lang/String;)V
#47 = Utf8 com/zhongyp/test/A
#48 = NameAndType #64:#15 // test:()V
#49 = Utf8 com/zhongyp/test/B
#50 = Class #65 // com/zhongyp/test/C
#51 = Utf8 BootstrapMethods
#52 = MethodHandle #6:#66 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#53 = MethodType #15 // ()V
#54 = MethodHandle #6:#67 // invokestatic com/zhongyp/test/Test.lambda$main$0:()V
#55 = NameAndType #68:#69 // run:()Ljava/lang/Runnable;
#56 = Utf8 com/zhongyp/test/Test
#57 = Utf8 java/lang/Object
#58 = Utf8 java/lang/System
#59 = Utf8 out
#60 = Utf8 Ljava/io/PrintStream;
#61 = Utf8 java/io/PrintStream
#62 = Utf8 println
#63 = Utf8 (Ljava/lang/String;)V
#64 = Utf8 test
#65 = Utf8 com/zhongyp/test/C
#66 = Methodref #70.#71 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#67 = Methodref #12.#72 // com/zhongyp/test/Test.lambda$main$0:()V
#68 = Utf8 run
#69 = Utf8 ()Ljava/lang/Runnable;
#70 = Class #73 // java/lang/invoke/LambdaMetafactory
#71 = NameAndType #74:#78 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#72 = NameAndType #39:#15 // lambda$main$0:()V
#73 = Utf8 java/lang/invoke/LambdaMetafactory
#74 = Utf8 metafactory
#75 = Class #80 // java/lang/invoke/MethodHandles$Lookup
#76 = Utf8 Lookup
#77 = Utf8 InnerClasses
#78 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#79 = Class #81 // java/lang/invoke/MethodHandles
#80 = Utf8 java/lang/invoke/MethodHandles$Lookup
#81 = Utf8 java/lang/invoke/MethodHandles

4.1 CONSTANT_Class_info结构

对类或接口的符号引用是从类或接口的二进制表示形式中的CONSTANT_Class_info结构第4.4.1节派生的。这样的引用给出了Class.getName方法返回的表单中的类或接口的名称。

对于非数组类或接口,名称是类或接口的二进制名称第4.2.1节

对于n维的数组类,名称以n个出现的ASCII“[”字符开头,后跟元素类型的表示:

  • 如果元素类型是基本类型,则它由相应的字段描述符第4.3.2节表示。

  • 否则,如果元素类型是引用类型,则它由ASCII“L”字符后跟元素类型的二进制名称第4.2.1节后跟ASCII“;”符号表示。

4.2 CONSTANT_Fieldref_info结构

对类或接口的字段的符号引用是从类或接口的二进制表示形式中的CONSTANT_Fieldref_info结构第4.4.2节派生的。这样的引用给出了字段的名称和描述符,以及对要在其中找到字段的类或接口的符号引用。

4.3 CONSTANT_Methodref_info结构

对类的方法的符号引用是从类或接口的二进制表示形式中的CONSTANT_Methodref_info结构第4.4.2节派生的。这样的引用给出了方法的名称和描述符,以及对要在其中找到方法的类的符号引用。

4.4 CONSTANT_InterfaceMethodref_info

对接口方法的符号引用是从类或接口的二进制表示形式中的CONSTANT_InterfaceMethodref_info结构第4.4.2节派生的。这样的引用给出了接口方法的名称和描述符,以及对要在其中找到方法的接口的符号引用。

4.5 CONSTANT_MethodHandle_info结构

方法句柄的符号引用是从类或接口的二进制表示形式中的CONSTANT_MethodHandle_info结构第4.4.8节派生的。这样的引用根据方法句柄的类型给出了类或接口的字段,类的方法或接口的方法的符号引用。

4.6 CONSTANT_MethodType_info结构

方法类型的符号引用是从类或接口的二进制表示形式中的CONSTANT_MethodType_info结构第4.4.9节派生的。这样的引用给出了方法描述符§4.3.3

4.7 CONSTANT_InvokeDynamic_info结构

invokedynamic instructions

A dynamic call site is originally in an unlinked state. In this state, there is no target method for the call site to invoke.
动态的调用site起初处在未连接的状态。在这种状态下,调用site没有调用的目标方法。
Before the JVM can execute a dynamic call site (an invokedynamic instruction), the call site must first be linked. Linking is accomplished by calling a bootstrap method which is given the static information content of the call site, and which must produce a method handle that gives the behavior of the call site.
在JVM可以执行动态调用site(invokedynamic指令)之前,必须首先连接调用site。连接是通过调用一个bootstrap方法来完成的,该方法被赋予了调用站点的静态信息内容,并且必须产生一个方法句柄来给出调用站点的行为。
Each invokedynamic instruction statically specifies its own bootstrap method as a constant pool reference. The constant pool reference also specifies the call site’s name and type descriptor, just like invokevirtual and the other invoke instructions.
每个invokedynamic指令静态的将它自己的引导方法指定作为一个常量池引用。常量池引用也指定调用site的名称和类型描述,就像invokevirtual和其他的调用描述一样。
Linking starts with resolving the constant pool entry for the bootstrap method, and resolving a MethodType object for the type descriptor of the dynamic call site. This resolution process may trigger class loading. It may therefore throw an error if a class fails to load. This error becomes the abnormal termination of the dynamic call site execution. Linkage does not trigger class initialization.
连接从解析引导方法的常量池条目开始,并为动态调用site的类型描述符解析MethodType对象。这个解决的进程可能触发类加载。如果一个类加载失败,可能因此抛出一个error。这个error将成为动态调用site执行的异常终止。连接不能触发类的初始化。
The bootstrap method is invoked on at least three values:
引导方法至少使用3个值调用:

  • a MethodHandles.Lookup, a lookup object on the caller class in which dynamic call site occurs
  • 一个是MethodHandles.Lookup,发生动态调用site的调用类上的一个lookup对象。
  • a String, the method name mentioned in the call site
  • 一个字符创,在调用site中提到的方法名称。
  • a MethodType, the resolved type descriptor of the call
  • 一个MethodType,已解析的调用的类型描述。
  • optionally, between 1 and 251 additional static arguments taken from the constant pool。
  • 可选地,从常量池中获取1到251个额外的静态参数。

对调用站点说明符的符号引用是从类或接口的二进制表示形式中的CONSTANT_InvokeDynamic_info结构第4.4.10节派生的。这样的参考给出:

  • 方法句柄的符号引用,它将作为invokedynamic指令的引导方法§invokedynamic;

  • 一系列符号引用(对类,方法类型和方法句柄),字符串文字和运行时常量值,它们将作为引导方法的静态参数;

  • 方法名称和方法描述符。

4.8 CONSTANT_String_info结构

此外,某些不是符号引用的运行时值是从constant_pool表中找到的项派生的:

字符串文字是对类String实例的引用,它是从类或接口的二进制表示形式的CONSTANT_String_info结构第4.4.3节派生而来的。 CONSTANT_String_info结构给出了构成字符串文字的Unicode代码点序列。

Java编程语言要求相同的字符串文字[即包含相同代码点序列的文字]必须引用类String的相同实例(JLS§3.10.5)。此外,如果在任何字符串上调用String.intern方法,则结果是对该字符串显示为文字时将返回的同一类实例的引用。因此,以下表达式的值必须为true:

("a"+"b"+"c").intern()=="abc"

为了派生字符串文字,Java虚拟机检查CONSTANT_String_info结构给出的代码点序列。

如果先前在类String的实例上调用了String.intern方法,该类包含与CONSTANT_String_info结构给出的Unicode代码点序列相同的Unicode代码点序列,则字符串文字派生的结果是对类String的同一实例的引用。

否则,将创建一个类String的新实例,其中包含CONSTANT_String_info结构给出的Unicode代码点序列;对该类实例的引用是字符串文字派生的结果。最后,调用新String实例的intern方法。

4.9 其他结构

运行时常量值是从类或接口的二进制表示形式中的CONSTANT_Integer_info,CONSTANT_Float_info,CONSTANT_Long_info或CONSTANT_Double_info结构第4.4.4节,第4.4.5节派生的。

请注意,CONSTANT_Float_info结构表示IEEE 754单一格式的值,CONSTANT_Double_info结构表示IEEE 754双格式§4.4.4,§4.4.5中的值。因此,从这些结构导出的运行时常数值必须是可以分别使用IEEE 754单格式和双格式表示的值。

类或接口的二进制表示的constant_pool表中的其余结构 - CONSTANT_NameAndType_info和CONSTANT_Utf8_info结构§4.4.6,§4.4.7 - 仅在派生对类,接口,方法,字段的符号引用时间接使用,方法类型和方法句柄,以及派生字符串文字和调用站点说明符时。

参考资料

Java动态语言支持 –周志明

深入理解Java虚拟机

Chapter 5. Loading, Linking, and Initializing

Package java.lang.invoke Description