JVM加载、启动和初始化

0 概述

这实际上是《The Java® Virtual Machine Specification - Java SE 8 Edition》中第五章内容(Loading, Linking, and Initializing)的部分翻译。主要目的是整理阅读笔记,让我自己看得明白,在这个前提下,尽量让别人看得明白,如果读者觉得我写得很混乱,还请自行阅读原文,不便之处敬请见谅。如有错误,还请指正(拉到页面底部点击『联系我』就可以发邮件给我)。

阅读本文内容需要先对java的class文件结构有所了解。如果尚不了解,不妨参考我的另一篇博文 Java class文件格式


  • 加载是这样一个过程:寻找一个特定名称的class或者interface的二进制表达形式(binary representation),然后从这个二进制表达形式中创建出一个class或者interface。
  • 链接是这样一个过程:取得class或者interface,然后将其结合到JVM的运行时状态,使得它可以被执行。
  • 初始化一个class或者interface的过程就是执行这个class或者interface的初始化方法<clinit>的过程。

1 运行时常量池(Run-Time Constant Pool)

在class或interface被创建的时候,用二进制形式的class文件中的constant_pool表来构造运行时常量池。运行时常量池中的所有引用最初都是符号引用。运行时常量池中的符号引用来自二进制表达形式中的以下结构:

Symbolic Reference to Structure in class file
class CONSTANT_Class_info
field CONSTANT_Fieldref_info
class method CONSTANT_Methodref_info
interface method CONSTANT_InterfaceMethodref_info
method handle CONSTANT_MethodHandle_info
method type CONSTANT_MethodType_info
call site specifier CONSTANT_InvokeDynamic_info

另外,某些不是符号引用的运行时值则来自constant_pool表中的以下结构:

  • 字符串字面量 CONSTANT_String_info
  • 运行时常量值 CONSTANT_Integer_info CONSTANT_Float_info CONSTANT_Long_info CONSTANT_Double_info

constant_pool里剩下的结构则只会被间接使用:

  • CONSTANT_NameAndType_info
  • CONSTANT_Utf8_info

2 JVM启动

JVM通过使用引导类加载器(bootstrap class loader)创建初始类来启动,初始类的指定方式与实现相关。

然后,JVM链接初始类,初始化它,并调用public static void main(String[])方法。此方法的调用将驱动所有进一步的执行。执行构成 main 方法的JVM指令可能会导致附加类和接口的链接(链接之后进而创建),以及调用其他方法。

在JVM的实现中,初始类可以作为命令行参数提供。或者, JVM的实现可以提供一个初始类来设置类加载程序,然后加载一个应用程序。可以选择其他初始类,只要它们与前一段中给出的规范一致即可。

3 创建和加载(Creation and Loading)

符号约定

class或者interface $C$ 用其类名 $N$ 来表示,其创建过程由另一个class or interface $D$ 触发,$D$通过运行时常量池引用$C$。class or interface的创建也可以通过$D$调用Java SE平台的特定class libraries中的方法来触发,例如反射。

如果$C$不是数组类,则通过使用class loader加载$C$的二进制表达来创建。

数组类没有外部的二进制表达,由JVM来创建,而不是class loader。

类加载器 $L$可以通过以下两种方式来创建$C$:

  • 直接定义$C$
  • 委托给另一个class loader

如果$L$委托了另一个class loader来加载$C$,则说$L$启动了$C$的加载($L$ initiates loading of $C$),或者等价地说,$L$是$C$的initiating loader。$N^L$ 表示由$L$ initiates loading的$C$。

如果$L$直接创建$C$,则我们说$L$定义了$C$($L$ defines $C$),或者等价地说,$L$是$C$的 defining loader。<$N, L$> 表示由$L$ defines的$C$。同时,$L$也是$C$的initiating loader

3.1 用Bootstrap Class Loader加载

下面的步骤用于加载、从而创建由$N$表示的、非数组类型的类或者接口$C$。

  1. 首先,JVM判断bootstrap class loader是否已经被标记为由$N$表示的类或接口的initiating loader。如果是,这个类或者接口就是$C$,后续没有创建过程。

    否则,JVM将$N$作为参数来调用bootstrap class loader的方法,用平台依赖的方式来搜索给出的$C$的表达形式。

    通常,类或接口会使用分层文件系统中的文件来表达,并且其名称会编码在文件的路径名(pathname)中。

    这个阶段需要检查以下错误:

    • 如果找不到给出的$C$的表达形式,抛出ClassNotFoundException
  2. 然后,JVM尝试使用加载算法,从给出的表达形式中取得$N$表示的类,将结果作为$C$。

3.2 用User-defined Class Loader加载

下面的步骤用于使用用户定义的类加载器(user-defined class loader) $L$加载、从而创建由$N$表示的、非数组类型的类或者接口$C$。

  1. 首先,JVM判断$L$是否已经被标记为由$N$表示的类或接口的initiating loader。如果是,这个类或者接口就是$C$,后续没有创建过程。
  2. 否则,JVM调用$L$的loadClass(N)方法,这个调用的返回值就是创建后的类或接口$C$。
  3. 然后JVM将$L$记录为$C$的initiating loader。

当使用$N$来调用$L$的loadClass方法时,$L$必须执行以下两个操作之一来加载$C$:

  1. $L$可以创建一个byte数组,这个数组表达了$C$的ClassFile结构;然后它必须调用ClassLoader类的defineClass方法,执行这个方法使得JVM用$L$使用加载算法从byte数组中取得$C$。
  2. $L$可以将加载过程委托给另一个类加载器$L’$,将参数$N$直接或者间接地传给$L’$的方法调用(一般是loadClass方法)。方法调用的结果是$C$。

在上述两个步骤中,无论处于任何原因无法加载$N$所表示的类或接口时,都必须抛出ClassNotFoundException

3.3 创建数组类(Array Classes)

下面的步骤用于使用类加载器$L$来加载、从而创建由$N$表示的、数组类型的类$C$。

如果$L$已被记录为与$N$相同的组件类型(component type)的数组类的initiating loader,则该类为$C$,并且不需要再进行任何数组类创建。

否则,执行下面的步骤来创建$C$:

  1. 如果组件类型是引用类型(reference type),则使用$L$递归地应用本节的算法,以加载、从而创建$C$的组件类型;
  2. JVM使用指定的组件类型和维数创建一个新的数组类。
    • 如果组件类型是一个引用类型,$C$会被标记为 『已被组件类型的defining class loader定义了』(having been defined by the defining class loader of the component type)。否则,$C$会被标记为『已被bootstrap class loader定义了』(having been defined by the bootstrap class loader)。
    • 无论如何,JVM都会将$L$记录为$C$的initiating loader。
    • 如果组件类型是一个引用类型,那么这个数组类的可见性(accessibility)和组件类型一致,否则可见性为public

3.4 加载的约束

在class loader出现的时候,确保类型安全链接要特别小心。有可能存在这样一种情况:两个不同的class loader触发了由$N$表示的class or interface的加载,而$N$在每个class loader里可能表示了不同的class or interface。

(TBD)

3.5 从class文件表达形式中获得Class

将一个非数组类型的class或者interface $C$ 记为 $N$,下面是用loader $L$从class文件格式中加载$C$为Class对象的步骤:

  1. JVM判断$L$是否已经被标记为$N$的initiating loader,如果是,创建过程将不可用,并且抛出LinkageError
  2. 否则,JVM尝试解析给出的表达。但是,给出的表达不一定是$C$的一个有效的表达。这个加载阶段必须检查以下错误:
    • 如果给出的表达形式不是ClassFile结构,抛出ClassFormatError
    • 否则,如果给出的表达不在支持的版本范围内(major version和minor version),抛出UnsupportedClassVersionError
    • 否则,如果给出的表达不是类名$N$的实际表达,抛出NoClassDefFoundError或者其子类
  3. 如果$C$有直接父类,就用Class and Interface Resolution算法来解析出$C$到其直接父类的符号链接。
    这个阶段要检查以下错误:
    • 如果其直接父类实际上是一个interface,抛出IncompatibleClassChangeError
    • 否则,如果$C$任意父类是$C$本身,抛出ClassCircularityError
  4. 如果$C$有任何直接父接口,则使用 Class and Interface Resolution 算法来解析出$C$到其直接父接口的符号链接。
    这个阶段要检查以下错误:
    • 如果其直接父接口实际上不是一个interface,抛出IncompatibleClassChangeError
    • 如果$C$的任何一个父接口是$C$本身,抛出ClassCircularityError
  5. JVM将$C$标记为拥有$L$作为其defining class loader,并且标记$L$是$C$的initiating loader

4 链接(Linking)

链接一个class或者interface包括验证准备阶段,涉及到的对象有:

  • 该class或interface本身
  • 其直接父类
  • 其直接父接口
  • 如果这是数组类型,还涉及其元素类型

解析class或interface中的符号链接是链接阶段可选的一部分。

该规范允许实现的灵活性,以便在链接活动(以及由于递归、加载)发生时,只要保持以下所有属性:

  • 一个class或interface在链接前要被完全加载
  • 一个class或interface在初始化前要被完全验证
  • 程序执行的一些操作可能直接或间接地要求链接涉及错误的class或interface,当这些错误被检测到时,必须在发生这些操作的地方抛出这些错误。

例如,一个JVM实现可能选择在使用到一个class或interface的时候才去解析其中的每个符号链接(懒加载或者延迟加载,”lazy” or “late” resolution),或者在验证class的时候一次性解析其中所有的符号链接(”eager” or “static” resolution)。这意味着在某些实现中,在class或interface初始化之后还有可能继续执行解析过程。无论采用哪种策略,在解析期间检测到的任何错误都必须在程序中的使用对class或interface的符号引用的地方抛出,不论是直接还是间接地使用到。

由于链接阶段涉及到新数据结构的分配(allocation),有可能因OutOfMEmoryError而失败。

4.1 验证(Verification)

验证阶段确保了class或interface的二进制表达在结构上是正确的。验证阶段有可能引起其他类或接口被加载,但是不需要导致它们被验证或者准备。

如果class或interface的二进制表达不满足The class File Format - Constraints on Java Virtual Machine Code中列出的静态约束或结构型约束,那么在程序中导致class或interface被校验的地方必须要抛出VerifyError

如果因为抛出了LinkageError(或子类)实例错误导致JVM尝试验证class或interface失败,则随后尝试验证class或interface始终会失败,并抛出相同的错误 作为初步验证尝试的结果。

4.2 准备(Preparation)

准备阶段包括创建class or interface的static fields,并初始化默认值。这个过程不需要执行任何JVM代码。静态字段的显式初始值设定是作为初始化的一部分执行,而不是准备阶段。

在class or interface $C$ 的准备阶段,JVM有以下约束:

令$L_1$为$C$的defining loader,$m$为$C$中覆盖自父类或者父接口<$D, L_2$>的方法,对于每个$m$,令其返回值为$T_r$,形参为$T_{f_1},…,T_{f_n}$,那么:

  • 如果$T_r$不是数组类型,令$T_0$为$T_r$;否则令$T_0$为$T_r$的元素类型;
  • 对于$i=1, …, n$,如果$T_{f_i}$不是数组类型,令$T_i$为$T_{f_i}$;否则令$T_i$为$T_{f_i}$的元素类型

则有:
$$ {T_i}^{L_1} = {T_i}^{L_2}, i=0, …, n $$

更进一步的情况,如果$C$实现了父接口<$I, L_3$>中的方法$m$,但是$C$没有声明方法$m$,但是$C$的父类<$D, L_2$>声明了方法$m$的实现,则有以下约束:

$m$的返回类型记为$T_r$,$m$的形参类型记为$T_{f1}, …, T_{fn}$,则:
如果$T_r$不是数组类型,令$T_0$为$T_r$,否则令$T_0$为$T_r$的元素类型(element type)。
对于所有$i=0, …, n$:如果$T_{fi}$不是数组类型,则$T_i$为$T_{fi}$,否则$T_i$为$T_{fi}$的元素类型。
那么有
$$ {T_i}^{L_2}={T_i}^{L_3}, i=0, …, n $$

4.3 解析(Resolution)

以下JVM指令对运行时常量池做了符号引用,执行任何这些指令都需要解析其符号引用:
anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, and putstatic

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

解析某次出现的invokedynamic指令中的符号引用并不意味着该符号引用对于其它任何invokedynamic指令来说都被解析了。

对于其他指令来说,解析了某次出现的指令中的符号引用,确实意味着该符号应用对于其他任意的非invokedynamic指令来说都视为被解析了。

上文的意思是,由一个特定的invokedynamic指令确定的具体值是一个绑定到该特定invokedynamic指令的call site object

(TBD)

下面部分阐述对一个class或interface $D$所引用的、尚在运行时常量池中的符号引用的解析过程。符号引用的类型不同,解析的细节也不同。

4.3.1 Class and Interface Resolution 类和接口解析

执行以下步骤来将$D$所引用的、未解析的符号引用解析为由$N$表示的class或interface $C$:

  1. 用$D$的defining class loader来创建由$N$表示的class或interface,细节在第三节(Creation and Loading)给出了。
    在创建过程中抛出的任何作为失败结果的exception都可以作为解析过程的失败结果抛出。
  2. 如果$C$是数组类并且其元素类型是一个引用类型,则递归地调用上一小节(Class and Interface Resolution)中的算法来解析其元素类型的符号引用。
  3. 最后,检查$C$的访问授权:
    • 如果$C$不能被$D$访问,抛出IllegalAccessError
      这种情况举例:如果$C$这个类本来是被声明为public的,但是在$D$编译完之后被改为了非public了。

如果第1、2步成功执行但是第3步失败了,$C$仍然是有效和可用的。尽管如此,这个解析过程也是失败了的,并且$D$也禁止访问$C$。

4.3.2 Field Resolution 字段解析

为了将$D$中未解析的符号引用解析为一个class或interface $C$中的一个字段(field),由字段引用(field reference)给出的到$C$的符号引用必须首先被解析(4.3.1)。

在解析字段引用的时候,字段解析(field resolution)首先尝试查找在$C$及其父类中引用的字段:

  1. 如果$C$使用了由字段引用指定的名称和描述符来声明一个字段,则字段查找(field lookup)成功。所声明的字段就是查找结果。
  2. 否则,对$C$的直接父接口递归地进行字段查找。
  3. 否则,如果$C$具有父类$S$,对$S$递归地进行字段查找。
  4. 否则,字段查找失败。

然后:

  • 如果字段查找失败了,字段解析抛出NoSuchFieldError
  • 否则,如果字段查找成功了,但是$D$不能访问该引用字段,抛出IllegalAccessError
  • 否则,令实际声明该引用字段的class或interface为<$E, L_1$>,令$L_2$为$D$的defining loading
    假设引用字段的类型为$T_f$,如果$T_f$不是数组类型,则$T$为$T_f$,如果$T_f$为数组类型,则$T$为$T_f$的元素类型。
    JVM必须保证$T^{L_1}=T^{L_2}$的约束。

4.3.3 Method Resolution 方法解析

为了将$D$中的符号引用解析为class $C$中的方法,由该方法引用给出的到$C$的符号引用要首先被解析(4.3.1)。

当解析一个方法引用时:

  1. 如果$C$是一个interface,抛出IncompatibleClassChangeError
  2. 否则,方法解析(method resolution)尝试在$C$及其父类中定位该引用方法:

    • 如果$C$刚好声明了一个由该方法引用指定的名字的方法,并且声明的方法是一个signature polymorphic method,那么方法查找成功。描述符中声明的所有类名都被解析了。

      The resolved method is the signature polymorphic method declaration. It is not necessary for C to declare a method with the descriptor specified by the method reference.

      signature polymorphic method这个概念在《The Java® Virtual Machine Specification - Java SE 8 Edition》的 2.9 Special Methods 中定义:

      A method is signature polymorphic if all of the following are true:
      • It is declared in the java.lang.invoke.MethodHandle class.
      • It has a single formal parameter of type Object[].
      • It has a return type of Object.
      • It has the ACC_VARARGS and ACC_NATIVE flags set.

    • 否则,如果$C$用该方法引用指定的名字和描述符声明了一个方法,方法查找成功。

    • 否则,如果$C$有父类,对$C$的直接父类递归地调用步骤2。
  3. 否则,方法解析尝试在$C$的父接口中定位引用方法:
    • If the maximally-specific superinterface methods of C for the name and descriptor specified by the method reference include exactly one method that does not have its ACC_ABSTRACT flag set, then this method is chosen and method lookup succeeds.
    • Otherwise, if any superinterface of C declares a method with the name and descriptor specified by the method reference that has neither its ACC_PRIVATE flag nor its ACC_STATIC flag set, one of these is arbitrarily chosen and method lookup succeeds.
    • Otherwise, method lookup fails.

A maximally-specific superinterface method of a class or interface $C$ for a particular method name and descriptor is any method for which all of the following are true:

  • The method is declared in a superinterface (direct or indirect) of $C$.
  • The method is declared with the specified name and descriptor.
  • The method has neither its ACC_PRIVATE flag nor its ACC_STATIC flag set.
  • Where the method is declared in interface I, there exists no other maximally- specific superinterface method of $C$ with the specified name and descriptor that is declared in a subinterface of $I$.

The result of method resolution is determined by whether method lookup succeeds or fails:

  • If method lookup fails, method resolution throws a NoSuchMethodError.
  • Otherwise, if method lookup succeeds and the referenced method is not
    accessible (§5.4.4) to $D$, method resolution throws an IllegalAccessError.
  • Otherwise,let<$E,L_1$>be the class or interface in which the referenced method $m$ is actually declared, and let $L_2$ be the defining loader of $D$.
    Given that the return type of $m$ is $T_r$, and that the formal parameter types of $m$
    are $T_{f_1}, …, T_{f_n}$, then:
    If $T_r$ is not an array type, let $T_0$ be $T_r$; otherwise, let $T_0$ be the element type (§2.4) of $T_r$.
    For $i = 1, …, n$: If $T_{f_i}$ is not an array type, let $T_i$ be $T_{f_i}$; otherwise, let $T_i$ be the element type (§2.4) of $T_{f_i}$.
    The Java Virtual Machine must impose the loading constraints $T^{L_1} = T^{L_2}$ for $i = 0, …, n$ (§5.3.4).
    When resolution searches for a method in the class’s superinterfaces, the best outcome is to identify a maximally-specific non-abstract method. It is possible that this method will be chosen by method selection, so it is desirable to add class loader constraints for it.
    Otherwise, the result is nondeterministic. This is not new: The Java® Virtual Machine Specification has never identified exactly which method is chosen, and how “ties” should be broken. Prior to Java SE 8, this was mostly an unobservable distinction. However, beginning with Java SE 8, the set of interface methods is more heterogenous, so care must be taken to avoid problems with nondeterministic behavior. Thus:

    • Superinterface methods that are private and static are ignored by resolution. This is consistent with the Java programming language, where such interface methods are not inherited.
    • Any behavior controlled by the resolved method should not depend on whether the method is abstract or not.

      Note that if the result of resolution is an abstract method, the referenced class $C$ may be non-abstract. Requiring $C$ to be abstract would conflict with the nondeterministic choice of superinterface methods. Instead, resolution assumes that the run time class of the invoked object has a concrete implementation of the method.

4.3.4 Interface Method Resolution

(TBD)

4.3.5 Method Type and Method Handle Resolution

(TBD)

4.3.6 Call Site Specifier Resolution

(TBD)

4.4 访问控制(Access Control)

当且仅当以下都为真时,class或interface $C$对class或interface $D$来说是可访问的:

  • $C$为public
  • $C$和$D$是同一个运行时包(run-time package)的成员。

当且仅当以下都为真时,一个字段或方法$R$对class或interface $D$来说是可访问的:

  • $R$为public
  • $R$为protected并且在类$C$中被声明,同时$D$是$C$的子类或者是$C$本身。更进一步,如果$R$不是static的,指向$R$的符号引用必须包含指向类$T$的符号引用,这个$T$是$D$的子类或者是$D$的父类或者是$D$本身;
  • $R$是protected的,或者具有默认的访问级别(即没有显式声明访问修饰符,非public、非protected、非private),并且和$D$是同一个运行时包(run-time package)的成员;
  • $R$是private的并且在$D$里声明。

上述访问控制的讨论省略了调用protected方法或者访问protected字段的目标的相关限制(目标必须是$D$或者是$D$的子类型)。这种约束是验证阶段的一部分,不是链接时的访问控制。

4.5 覆盖(Overriding)

有$C$类中声明的实例方法$m_C$和$A$类中声明的另一个实例方法$m_A$,当$m_C$和$m_A$一样或者下列条件都为真时,我们说$m_C$覆盖了$m_A$:

  • $C$是$A$的子类
  • $m_C$和$m_A$具有同样的名称和描述符
  • $M_C$没有标记为ACC_PRIVATE
  • 以下其中一个为真:
    • $m_A$被标记为ACC_PUBLIC;或者被标记为ACC_PROTECTED;或者都没有标记为ACC_PUBLICACC_PROTECTEDACC_PRIVATE并且$A$和$C$属于同一个运行时包。
    • $m_C$覆盖了方法$m’$($m’$与$m_C$和$m_A$都不同),而$m’$覆盖了$m_A$

5 初始化(Initialization)

(TBD)

6 绑定本地方法实现(Binding Native Method Implementations)

绑定是这样一个过程:一种用Java以外的语言编写的、实现本地方法的函数被集成到JVM中以便执行。

虽然这个过程通常被称为链接,但术语『绑定』在这个规范中用来避免与JVM的类或接口链接混淆。

7 JVM Exit

某个线程调用了RuntimeSystem类的exit方法或者Runtime类的halt方法、并且exithalt操作得到了安全管理器(security manager)的准许时,JVM退出。

另外,JNI(Java Native Interface)规范描述了当JNI Invocation API被用于加载和卸载JVM时JVM的终止。

Reference