Appearance
第5章 加载、链接和初始化
提示
来自deepseek解释
原文链接:https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-5.html
Java虚拟机动态地加载、链接和初始化类和接口。加载(Loading)是查找具有特定名称的类或接口类型的二进制表示,并从该二进制表示创建类或接口的过程。链接(Linking)是获取一个类或接口并将其合并到Java虚拟机的运行时状态以便其可以执行的过程。类或接口的初始化(Initialization)包括执行类或接口初始化方法(§2.9.2)。
本章中,§5.1描述了Java虚拟机如何从类或接口的二进制表示中派生符号引用。§5.2解释了加载、链接和初始化过程如何由Java虚拟机首次启动。§5.3规定了类加载器如何加载类和接口的二进制表示,以及类和接口是如何被创建的。链接在§5.4中描述。§5.5详细说明了类和接口如何初始化。§5.6介绍了绑定本地方法的概念。最后,§5.7描述了Java虚拟机何时退出。
5.1 运行时常量池
Java虚拟机为每个类和接口维护一个运行时常量池(§2.5.5)。该数据结构服务于传统编程语言实现中符号表的许多目的。类或接口的二进制表示(§4.4)中的constant_pool表用于在类或接口创建时(§5.3)构造运行时常量池。
运行时常量池中有两种条目:符号引用(symbolic references),可以在之后被解析(§5.4.3);以及静态常量(static constants),不需要进一步处理。
运行时常量池中的符号引用根据每个条目的结构从constant_pool表中的条目派生而来:
对类或接口的符号引用派生自
CONSTANT_Class_info结构(§4.4.1)。这样的引用以如下形式给出类或接口的名称:对类或接口的字段的符号引用派生自
CONSTANT_Fieldref_info结构(§4.4.2)。这样的引用给出字段的名称和描述符,以及对该字段所在类或接口的符号引用。对类的方法的符号引用派生自
CONSTANT_Methodref_info结构(§4.4.2)。这样的引用给出方法的名称和描述符,以及对该方法所在类的符号引用。对接口的方法的符号引用派生自
CONSTANT_InterfaceMethodref_info结构(§4.4.2)。这样的引用给出接口方法的名称和描述符,以及对该方法所在接口的符号引用。对方法句柄的符号引用派生自
CONSTANT_MethodHandle_info结构(§4.4.8)。这样的引用根据方法句柄的种类,给出对类或接口的字段、类的方法或接口的方法的符号引用。对方法类型的符号引用派生自
CONSTANT_MethodType_info结构(§4.4.9)。这样的引用给出一个方法描述符(§4.3.3)。对动态计算常量的符号引用派生自
CONSTANT_Dynamic_info结构(§4.4.10)。对动态计算调用点的符号引用派生自
CONSTANT_InvokeDynamic_info结构(§4.4.10)。这样的引用给出:- 对方法句柄的符号引用,该句柄将在
invokedynamic指令(§invokedynamic)过程中被调用来计算java.lang.invoke.CallSite的实例; - 一组符号引用和静态常量,当方法句柄被调用时将作为静态参数;
- 一个非限定名称和一个方法描述符。
- 对方法句柄的符号引用,该句柄将在
运行时常量池中的静态常量也根据每个条目的结构从constant_pool表中的条目派生而来:
字符串常量是对
String类实例的引用,派生自CONSTANT_String_info结构(§4.4.3)。为了派生字符串常量,Java虚拟机检查CONSTANT_String_info结构给出的码点序列。数值常量派生自
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双精度格式的值。因此,从这些结构派生的数值常量必须是能够分别使用IEEE 754单精度和双精度格式表示的值。
constant_pool表中剩余的结构——描述性结构CONSTANT_NameAndType_info、CONSTANT_Module_info和CONSTANT_Package_info,以及基础结构CONSTANT_Utf8_info——仅在构造运行时常量池时间接使用。运行时常量池中没有直接对应于这些结构的条目。
运行时常量池中的某些条目是可加载的(loadable),这意味着:
如果运行时常量池中的条目派生自constant_pool表中可加载的条目(参见表4.4-C),则该条目是可加载的。
5.2 Java虚拟机启动
Java虚拟机通过使用引导类加载器(§5.3.1)或用户自定义类加载器(§5.3.2)创建一个初始类或接口来启动。然后Java虚拟机链接该初始类或接口,初始化它,并调用public static void main(String[])方法。该方法的调用驱动所有进一步的执行。构成main方法的Java虚拟机指令的执行可能导致额外类和接口的链接(以及随之而来的创建),以及额外方法的调用。
初始类或接口以依赖于实现的方式指定。例如,初始类或接口可以作为命令行参数提供。或者,Java虚拟机的实现本身可以提供一个初始类,该类设置一个类加载器,然后由该类加载器加载应用程序。只要与上一段给出的规范一致,其他初始类或接口的选择也是可能的。
5.3 创建与加载
由名称N表示的类或接口C的创建,包括在Java虚拟机的方法区(§2.5.4)中构造C的特定于实现的内部表示。
类或接口的创建由另一个类或接口D触发,D的运行时常量池通过名称N符号引用C(§5.4.3.1)。如果N不表示数组类,则Java虚拟机依赖类加载器来定位名为N的类或接口的二进制表示(§4.1)。一旦类加载器定位了二进制表示,它就转而依赖Java虚拟机从该二进制表示派生出类或接口C,然后在方法区中创建C。数组类没有外部的二进制表示;它们由Java虚拟机通过不同的过程创建。
类或接口的创建也可能由D调用某些Java SE平台类库(§2.12)中的方法(如反射)来触发。
有两种类加载器:Java虚拟机提供的引导类加载器(bootstrap class loader),以及用户自定义类加载器(user-defined class loaders)。每个用户自定义类加载器都是抽象类ClassLoader的子类的实例。应用程序使用用户自定义类加载器来扩展Java虚拟机动态创建类的方式。用户自定义类加载器可用于创建来自用户自定义来源的类。例如,类可以通过网络下载、即时生成或从加密文件中提取。
当Java虚拟机要求类加载器L定位名为N的类或接口的二进制表示时,L加载由N表示的类或接口C。L可以直接加载C,即定位一个二进制表示并要求Java虚拟机从该二进制表示派生出C并创建它。或者,L可以间接加载C,即委托给另一个类加载器,由后者直接或间接地加载C。
如果L直接加载C,我们说L定义了C,或者等效地说,L是C的定义加载器(defining loader)。无论L是直接还是间接加载C,我们都称L发起了C的加载,或者等效地说,L是C的发起加载器(initiating loader)。
由于类加载器的委托机制,在Java虚拟机请求时发起加载的加载器L1可能与通过定义类或接口来完成加载的加载器L2不同。在这种情况下,我们说L1和L2各自发起了C的加载,或者说L1和L2各自是C的发起加载器。在L1和L2之间的委托链中的任何加载器都不被视为C的发起加载器。
加载一个类或接口是Java虚拟机和类加载器(如果发生委托,则是多个类加载器)之间的共同努力,这应该是清楚的。加载的最终结果是Java虚拟机在其方法区中创建一个类或接口,因此通常可以方便地说一个类或接口被加载从而被创建。加载的复杂的来回交互性质,加上用户自定义类加载器表现出任意行为的能力,意味着在Java虚拟机创建了类或接口之后、但在参与加载的每个类加载器完成之前,可能会抛出异常。本规范在通常所说的“加载并创建类或接口的过程”中考虑了这些异常。
Java虚拟机使用以下三种过程之一来创建由类或接口D的运行时常量池中的名称N表示的类或接口C:
- 如果
N表示一个非数组类或接口,并且D由引导类加载器定义,则引导类加载器发起C的加载(§5.3.1)。 - 如果
N表示一个非数组类或接口,并且D由用户自定义类加载器定义,则同一个用户自定义类加载器发起C的加载(§5.3.2)。 - 如果
N表示一个数组类,则Java虚拟机与D的定义加载器相关联地创建由N表示的数组类C(§5.3.3)。尽管D的定义加载器在创建数组类的过程中是相关的,但它不会被用来加载从而创建该数组类。
如果在类或接口的加载过程中发生错误——无论是在类加载器定位二进制表示时,还是在Java虚拟机从中派生并创建类时——该错误必须在程序(直接或间接)使用正在加载的类或接口的位置抛出。
一个行为良好的类加载器应维护三个属性:
- 给定相同的名称,一个好的类加载器应始终返回相同的
Class对象。 - 如果类加载器
L1将类C的加载委托给另一个加载器L2,那么对于作为C的直接超类或直接超接口、或作为C中字段的类型、或作为C中方法或构造器的形参类型、或作为C中方法的返回类型的任何类型T,L1和L2应返回相同的Class对象。 - 如果用户自定义类加载器预取类和接口的二进制表示,或一起加载一组相关的类,则它必须仅在程序中如果没有预取或分组加载则可能发生加载错误的位置反映加载错误。
创建之后,一个类或接口不仅仅由其名称决定,而是由一个二元组决定:其二进制名称(§4.2.1)及其定义加载器。每个这样的类或接口属于一个单独的运行时包。类或接口的运行时包由该包名称和该类或接口的定义加载器决定。
5.3.1 使用引导类加载器加载
使用引导类加载器加载并创建由N表示的非数组类或接口C的过程如下。
首先,Java虚拟机确定引导类加载器是否已被记录为以N表示的类或接口的发起加载器。如果是,则这个类或接口就是C,不需要进行类加载或创建。
否则,Java虚拟机将参数N传递给对引导类加载器上某个方法的调用。为了加载C,引导类加载器以平台相关的方式定位C的声称表示,然后要求Java虚拟机使用引导类加载器从声称表示中派生出由N表示的类或接口C,并通过§5.3.5的算法创建C。
通常,类或接口将使用分层文件系统中的文件来表示,并且类或接口的名称将被编码在文件的路径名中以帮助定位它。
如果未找到C的声称表示,引导类加载器抛出ClassNotFoundException。加载并创建C的过程随后失败,并抛出NoClassDefFoundError,其原因为该ClassNotFoundException。
如果找到了C的声称表示,但从声称表示派生出C失败,则加载并创建C的过程因同样的原因失败。
否则,加载并创建C的过程成功。
5.3.2 使用用户自定义类加载器加载
使用用户自定义类加载器L加载并创建由N表示的非数组类或接口C的过程如下。
首先,Java虚拟机确定L是否已被记录为以N表示的类或接口的发起加载器。如果是,则这个类或接口就是C,不需要进行类加载或创建。
否则,Java虚拟机在L上调用ClassLoader类的loadClass方法,传递类或接口的名称N。L必须执行以下两种操作之一来加载从而创建类或接口C:
类加载器
L可以直接加载C。这通过获取一个声称表示C为ClassFile结构(§4.1)的字节数组,然后调用ClassLoader类的defineClass方法来实现。调用defineClass会使Java虚拟机使用L从该字节数组派生出由N表示的类或接口C,并通过§5.3.5的算法创建C。L应使用defineClass的结果作为loadClass的结果。类加载器
L可以间接加载C,即将C的加载委托给某个其他类加载器L'。这通过将参数N传递给对L'上某个方法的调用(通常是ClassLoader类的loadClass方法)来实现。L应使用该调用的结果作为loadClass的结果。
无论执行哪种操作,以下规则都适用:
- 如果类加载器找不到由
N表示的类或接口的声称表示,它必须抛出ClassNotFoundException。加载并创建C的过程随后失败,并抛出NoClassDefFoundError,其原因为该ClassNotFoundException。 - 如果类加载器找到了
C的声称表示,但从声称表示派生出C失败,则加载并创建C的过程因同样的原因失败。 - 如果类加载器抛出了
ClassNotFoundException以外的异常,则加载并创建C的过程因同样的原因失败。
如果在L上调用loadClass有结果,则:
- 如果结果为
null,或者结果是一个名称不是N的类或接口,则丢弃该结果,加载并创建的过程失败并抛出NoClassDefFoundError。 - 否则,结果就是已创建的类或接口
C。Java虚拟机记录L是C的发起加载器(§5.3.4)。加载并创建C的过程成功。
自JDK 1.1以来,Oracle的Java虚拟机实现调用类加载器上的单参数loadClass方法以使其加载类或接口。loadClass的参数是要加载的类或接口的名称。还有一个双参数版本的loadClass方法,其中第二个参数是一个布尔值,指示是否要链接该类或接口。JDK 1.0.2中只提供了双参数版本,Oracle的Java虚拟机实现依赖它来链接已加载的类或接口。从JDK 1.1开始,Oracle的Java虚拟机实现直接链接类或接口,而不依赖类加载器。
5.3.3 创建数组类
以下步骤用于创建与类加载器L相关联的、由名称N表示的数组类C。L可以是引导类加载器或用户自定义类加载器。
首先,Java虚拟机确定L是否已被记录为具有与N相同组件类型的数组类的发起加载器。如果是,则这个类就是C,不需要创建数组类。
否则,执行以下步骤来创建C:
- 如果组件类型是引用类型,则使用
L递归应用本节(§5.3)的算法,以加载从而创建C的组件类型。 - Java虚拟机创建一个具有指定组件类型和维度数量的新数组类。
- 如果组件类型是引用类型,则Java虚拟机将
C标记为具有组件类型的定义加载器作为其定义加载器。否则,Java虚拟机将C标记为具有引导类加载器作为其定义加载器。 - 在任何情况下,Java虚拟机随后记录
L是C的发起加载器(§5.3.4)。 - 如果组件类型是引用类型,则数组类的可访问性由其组件类型的可访问性决定(§5.4.4)。否则,该数组类对所有类和接口都是可访问的。
5.3.4 加载约束
在存在类加载器的情况下确保类型安全的链接需要特别小心。当两个不同的类加载器发起对由N表示的类或接口的加载时,名称N可能在每个加载器中表示不同的类或接口。
当一个类或接口C = <N1, L1>对另一个类或接口D = <N2, L2>的字段或方法进行符号引用时,该符号引用包含一个描述符,指定字段的类型,或方法的返回类型和参数类型。至关重要的是,字段或方法描述符中提到的任何类型名称N,在由L1加载时和由L2加载时必须表示相同的类或接口。
为确保这一点,Java虚拟机在准备(§5.4.2)和解析(§5.4.3)期间施加形如NL1 = NL2的加载约束。为了实施这些约束,Java虚拟机将在某些规定的时间(参见§5.3.1、§5.3.2、§5.3.3和§5.3.5)记录特定加载器是特定类的发起加载器。在记录了一个加载器是一个类的发起加载器之后,Java虚拟机必须立即检查是否有任何加载约束被违反。如果是,则该记录被撤回,Java虚拟机抛出LinkageError,导致该记录的加载操作失败。
类似地,在施加了加载约束(参见§5.4.2、§5.4.3.2、§5.4.3.3和§5.4.3.4)之后,Java虚拟机必须立即检查是否有任何加载约束被违反。如果是,则新施加的加载约束被撤回,Java虚拟机抛出LinkageError,导致施加该约束的操作(解析或准备,视情况而定)失败。
这里描述的情况是Java虚拟机检查是否有任何加载约束被违反的唯一时刻。当且仅当以下四个条件都成立时,加载约束才被违反:
(此处原文列出了四个条件,但获取的文本中未完全显示,故省略具体内容。关于类加载器和类型安全的更全面讨论超出了本规范的范围。感兴趣的读者可参考Sheng Liang和Gilad Bracha所著的《Dynamic Class Loading in the Java Virtual Machine》(Proceedings of the 1998 ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages and Applications)。)
5.3.5 从class文件表示派生类
以下步骤用于使用类加载器L,从class文件格式的声称表示中派生出由名称N表示的非数组类或接口C。
首先,Java虚拟机确定
L是否已被记录为以N表示的类或接口的发起加载器。如果是,则此派生尝试无效,派生抛出LinkageError。否则,Java虚拟机尝试解析该声称表示。该声称表示实际上可能不是
C的有效表示,因此派生必须检测以下问题:- 如果声称表示不是
ClassFile结构(§4.1、§4.8),派生抛出ClassFormatError。 - 否则,如果声称表示不是受支持的主版本或次版本(§4.1),派生抛出
UnsupportedClassVersionError。UnsupportedClassVersionError是ClassFormatError的子类,在JDK 1.2中引入,以便于识别由于尝试加载使用不受支持的class文件格式版本的类而导致的ClassFormatError。在JDK 1.1及更早版本中,在出现不受支持的版本的情况下,会抛出NoClassDefFoundError或ClassFormatError的实例,具体取决于该类是由系统类加载器还是用户自定义类加载器加载。 - 否则,如果声称表示实际上并不表示名为
N的类或接口,派生抛出NoClassDefFoundError。当声称表示的this_class项指定了N以外的名称,或者access_flags项设置了ACC_MODULE标志时,会发生这种情况。
- 如果声称表示不是
如果
C有直接超类,则使用§5.4.3.1的算法解析从C到其直接超类的符号引用。注意,如果C是一个接口,它必须将Object作为其直接超类,并且Object必须已经被加载。只有Object没有直接超类。任何由于类或接口解析失败而可能抛出的异常都可以作为派生的结果抛出。此外,派生必须检测以下问题:
- 如果
C的任何超类是C本身,派生抛出ClassCircularityError。 - 否则,如果命名为
C的直接超类的类或接口实际上是一个接口或final类,派生抛出IncompatibleClassChangeError。 - 否则,如果命名为
C的直接超类的类具有PermittedSubclasses属性(§4.7.31),并且以下任何条件为真,派生抛出IncompatibleClassChangeError:- 该超类与
C处于不同的运行时模块(§5.3.6)。 C未设置其ACC_PUBLIC标志(§4.1),并且该超类与C处于不同的运行时包(§5.3)。- 超类的
PermittedSubclasses属性的classes数组中没有条目引用名称为N的类或接口。
- 该超类与
- 否则,如果
C是一个类,并且C中声明的某个实例方法可以覆盖(§5.4.5)在C的超类中声明的final实例方法,派生抛出IncompatibleClassChangeError。
- 如果
如果
C有任何直接超接口,则使用§5.4.3.1的算法解析从C到其直接超接口的符号引用。任何由于类或接口解析失败而可能抛出的异常都可以作为派生的结果抛出。此外,派生必须检测以下问题:
- 如果
C的任何超接口是C本身,派生抛出ClassCircularityError。 - 否则,如果命名为
C的直接超接口的任何类或接口实际上不是一个接口,派生抛出IncompatibleClassChangeError。 - 否则,对于
C命名的每个直接超接口,如果该超接口具有PermittedSubclasses属性(§4.7.31),并且以下任何条件为真,派生抛出IncompatibleClassChangeError:- 该超接口与
C处于不同的运行时模块。 C未设置其ACC_PUBLIC标志,并且该超接口与C处于不同的运行时包。- 超接口的
PermittedSubclasses属性的classes数组中没有条目引用名称为N的类或接口。
- 该超接口与
- 如果
如果在步骤1-4中没有抛出异常,则类或接口C的派生成功。Java虚拟机将C标记为以L作为其定义加载器,记录L是C的发起加载器(§5.3.4),并在方法区(§2.5.4)中创建C。
当派生成功时,加载和创建C的过程直到每个(直接或间接)参与加载C的类加载器都返回C作为其结果时才完成。根据用户自定义类加载器的行为,加载和创建C的过程仍可能失败(§5.3.2)。
如果在步骤1-4中抛出异常,则类或接口C的派生因该异常而失败。
5.3.6 模块与层
Java虚拟机支持将类和接口组织成模块(modules)。类或接口C在模块M中的成员身份用于控制从M以外的模块中的类和接口对C的访问(§5.4.4)。
模块成员身份根据运行时包(§5.3)来定义。程序确定每个模块中包的名称,以及将创建这些命名包的类和接口的类加载器;然后,它将包和类加载器指定给ModuleLayer类的defineModules方法的调用。调用defineModules会导致Java虚拟机创建新的运行时模块,这些模块与类加载器的运行时包相关联。
每个运行时模块指示它导出的运行时包,这影响对这些运行时包中公共类和接口的访问。每个运行时模块还指示它读取的其他运行时模块,这影响其自身代码对这些运行时模块中公共类型和接口的访问。
我们说一个类位于一个运行时模块中,当且仅当该类的运行时包与该运行时模块相关联(或如果该类实际被创建,则将与之关联)。
由类加载器创建的类恰好位于一个运行时包中,因此恰好位于一个运行时模块中,因为Java虚拟机不支持一个运行时包与多个运行时模块相关联(或更形象地说,“拆分”到多个运行时模块中)。
一个运行时模块通过defineModules的语义隐式地绑定到恰好一个类加载器。另一方面,一个类加载器可以在多个运行时模块中创建类,因为Java虚拟机不要求类加载器的所有运行时包都关联到同一个运行时模块。
换句话说,类加载器和运行时模块之间的关系不必是1:1的。对于要加载的一组给定模块,如果程序可以确定每个模块中包的名称仅在该模块中找到,则程序可以仅指定一个类加载器给defineModules的调用。这个类加载器将在多个运行时模块中创建类。
由defineModules创建的每个运行时模块都是一个层(layer)的一部分。一个层代表一组类加载器,它们共同服务于在一组运行时模块中创建类。有两种层:由Java虚拟机提供的引导层(boot layer)和用户自定义层(user-defined layers)。引导层在Java虚拟机启动时以依赖于实现的方式创建。它将标准运行时模块java.base与由引导类加载器定义的标准运行时包(如java.lang)关联起来。用户自定义层由程序创建,以构建依赖于java.base和其他标准运行时模块的运行时模块集。
一个运行时模块通过defineModules的语义隐式地是恰好一个层的一部分。然而,一个类加载器可以在不同层的运行时模块中创建类,因为相同的类加载器可以被指定给多次defineModules调用。访问控制由类的运行时模块决定,而不是由创建该类的类加载器或该类加载器所服务的层决定。
为一个层指定的类加载器集合,以及作为该层一部分的运行时模块集合,在层创建后是不可变的。然而,ModuleLayer类为程序提供了对用户自定义层中运行时模块之间关系的动态控制程度。
如果用户自定义层包含多个类加载器,则这些类加载器之间的任何委托都是创建该层的程序的责任。Java虚拟机不检查层的类加载器是否根据层的运行时模块读取彼此的方式相互委托。此外,如果层的运行时模块通过ModuleLayer类被修改以读取额外的运行时模块,则Java虚拟机不检查层的类加载器是否通过某些带外机制被修改以进行相应的委托。
层与类加载器既有相似之处也有不同之处。一方面,层类似于类加载器,因为每个都可以分别委托给一个或多个父层或类加载器,这些父层或类加载器先前分别创建了模块或类。也就是说,为一个层指定的模块集可能依赖于未为该层指定、而是先前为一个或多个父层指定的模块。另一方面,一个层只能被用于创建新模块一次,而一个类加载器可以通过多次调用defineClass方法在任何时候创建新类或接口。
一个类加载器有可能在一个未与任何该类加载器所服务的层关联的运行时包中定义一个类或接口。如果该运行时包体现了一个未指定给defineModules的命名包,或者该类或接口具有简单二进制名称(§4.2.1)因而属于体现未命名包(JLS §7.4.2)的运行时包,则可能发生这种情况。在任何一种情况下,该类或接口都被视为与该类加载器隐式绑定的特殊运行时模块的成员。这个特殊运行时模块被称为该类加载器的未命名模块(unnamed module)。该类或接口的运行时包与该类加载器的未命名模块相关联。未命名模块有特殊规则,旨在最大化它们与其他运行时模块的互操作性,如下:
- 一个类加载器的未命名模块与绑定到同一类加载器的所有其他运行时模块都是不同的。
- 一个类加载器的未命名模块与绑定到其他类加载器的所有运行时模块(包括未命名模块)都是不同的。
- 每个未命名模块读取每个运行时模块。
- 每个未命名模块将与其自身关联的每个运行时包导出到每个运行时模块。
5.4 链接
链接一个类或接口涉及验证和准备该类或接口、其直接超类、其直接超接口及其元素类型(如果它是数组类型),如有必要。链接还涉及解析该类或接口中的符号引用,但不一定与该类或接口被验证和准备同时进行。
本规范允许实现在链接活动(以及由于递归而导致的加载)发生的时间上具有灵活性,前提是满足以下所有属性:
- 一个类或接口在链接之前必须被完全加载。
- 一个类或接口在初始化之前必须被完全验证和准备。
- 链接期间检测到的错误必须在程序中某个点抛出,该点程序执行了可能直接或间接需要链接到涉及错误的类或接口的某个操作。
- 对动态计算常量的符号引用直到以下情况之一时才被解析:(i)执行引用它的
ldc、ldc_w或ldc2_w指令,或(ii)将其作为静态参数引用的引导方法被调用。 - 对动态计算调用点的符号引用直到将其作为静态参数引用的引导方法被调用时才被解析。
例如,Java虚拟机实现可以选择“惰性”链接策略,即类或接口中的每个符号引用(上述符号引用除外)在使用时单独解析。或者,实现可以选择“急切”链接策略,即在验证类或接口时一次性解析所有符号引用。这意味着在某些实现中,解析过程可能在类或接口被初始化之后继续进行。无论采用哪种策略,在解析期间检测到的任何错误都必须在程序(直接或间接)使用对该类或接口的符号引用的点抛出。
由于链接涉及新数据结构的分配,它可能因OutOfMemoryError而失败。
5.4.1 验证
验证(§4.10)确保类或接口的二进制表示在结构上是正确的(§4.9)。验证可能导致额外的类和接口被加载(§5.3),但不需要它们被验证或准备。
如果类或接口的二进制表示不满足§4.9中列出的静态或结构约束,则必须在导致该类或接口被验证的程序点抛出VerifyError。
如果Java虚拟机尝试验证类或接口由于抛出了LinkageError(或其子类)的实例而失败,则后续尝试验证该类或接口始终失败,并抛出与初始验证尝试相同的错误。
5.4.2 准备
准备涉及为类或接口创建静态字段并将这些字段初始化为其默认值(§2.3、§2.4)。这不需要执行任何Java虚拟机代码;静态字段的显式初始化器作为初始化的一部分(§5.5)执行,而不是准备。
在准备类或接口C期间,Java虚拟机还施加加载约束(§5.3.4):
设
L1是C的定义加载器。对于C中声明的每个可以覆盖(§5.4.5)超类或超接口<D, L2>中声明的实例方法的实例方法m,Java虚拟机施加加载约束如下。给定
m的返回类型为Tr,m的形式参数类型为Tf1, ..., Tfn:- 如果
Tr不是数组类型,令T0为Tr;否则,令T0为Tr的元素类型。 - 对于
i = 1到n:如果Tfi不是数组类型,令Ti为Tfi;否则,令Ti为Tfi的元素类型。 - 则对于
i = 0到n,施加约束TiL1 = TiL2。
- 如果
对于
C的超接口<I, L3>中声明的每个实例方法m,如果C本身没有声明可以覆盖m的实例方法,则相对于C和方法m(在<I, L3>中)选择一个方法(§5.4.6)。设<D, L2>是声明所选方法的类或接口。Java虚拟机施加加载约束如下。给定
m的返回类型为Tr,m的形式参数类型为Tf1, ..., Tfn:- 如果
Tr不是数组类型,令T0为Tr;否则,令T0为Tr的元素类型。 - 对于
i = 1到n:如果Tfi不是数组类型,令Ti为Tfi;否则,令Ti为Tfi的元素类型。 - 则对于
i = 0到n,施加约束TiL2 = TiL3。
- 如果
准备可能在创建之后的任何时间发生,但必须在初始化之前完成。
5.4.3 解析
许多Java虚拟机指令——anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic——依赖于运行时常量池中的符号引用。执行任何这些指令都需要解析该符号引用。
解析(Resolution)是从运行时常量池中的符号引用动态确定一个或多个具体值的过程。最初,运行时常量池中的所有符号引用都是未解析的。
对(i)类或接口、(ii)字段、(iii)方法、(iv)方法类型、(v)方法句柄或(vi)动态计算常量的未解析符号引用的解析,按照§5.4.3.1至§5.4.3.5中给出的规则进行。在前三节中,符号引用出现的类或接口被标记为D。然后:
- 如果在解析符号引用期间没有发生错误,则解析成功。后续尝试解析该符号引用总是简单成功,并产生与初始解析相同的实体。如果符号引用是动态计算常量,则这些后续尝试不会重新执行引导方法。
- 如果在解析符号引用期间发生错误,则该错误要么是(i)
IncompatibleClassChangeError(或其子类)的实例;(ii)由引导方法的解析或调用引发的Error(或其子类)的实例;或(iii)由于类加载失败或加载器约束被违反而引发的LinkageError(或其子类)的实例。该错误必须在程序(直接或间接)使用该符号引用的点抛出。后续尝试解析该符号引用始终失败,并抛出与初始解析尝试相同的错误。如果符号引用是动态计算常量,则这些后续尝试不会重新执行引导方法。
由于在初始解析尝试中发生的错误会在后续尝试中再次抛出,一个模块中的类如果试图通过解析其运行时常量池中的符号引用来访问不同模块中未导出的公共类型,即使在该类首次尝试之后的某个时间使用Java SE平台API动态导出了该公共类型的包,也始终会收到相同的错误,指示类型不可访问(§5.4.4)。
对动态计算调用点的未解析符号引用的解析按照§5.4.3.6中给出的规则进行。然后:
- 如果在解析符号引用期间没有发生错误,则解析仅对
class文件中需要解析的指令成功。该指令的操作码必定是invokedynamic。后续由该class文件中的同一指令尝试解析该符号引用总是简单成功,并产生与初始解析相同的实体。引导方法不会为这些后续尝试重新执行。对于class文件中所有其他指令(无论操作码如何)指示与上述invokedynamic指令相同的运行时常量池条目,该符号引用仍然未解析。 - 如果在解析符号引用期间发生错误,则错误类型同上。该错误必须在程序(直接或间接)使用该符号引用的点抛出。后续由同一
class文件中的同一指令尝试解析该符号引用始终失败,并抛出与初始解析尝试相同的错误。引导方法不会为这些后续尝试重新执行。对于class文件中所有其他指令(无论操作码如何)指示与上述invokedynamic指令相同的运行时常量池条目,该符号引用仍然未解析。
上述某些指令在解析符号引用时需要额外的链接检查。例如,为了使getfield指令成功解析其操作所涉及的字段的符号引用,它不仅必须完成§5.4.3.2中给出的字段解析步骤,还必须检查该字段不是static。如果它是一个static字段,则必须抛出链接异常。
由特定Java虚拟机指令执行所特有的检查生成的链接异常在该指令的描述中给出,并且不在解析的一般讨论中涵盖。注意,此类异常尽管被描述为Java虚拟机指令执行的一部分而不是解析的一部分,但仍然被适当地视为解析失败。
5.4.3.1 类与接口解析
为了解析从D到由N表示的类或接口C的未解析符号引用,执行以下步骤:
使用
D的定义加载器来加载从而创建由N表示的类或接口。此类或接口为C。过程的细节在§5.3中给出。任何由于加载和创建C失败而可能抛出的异常都可以作为类与接口解析失败的结果抛出。如果
C是一个数组类且其元素类型是引用类型,则递归调用§5.4.3.1中的算法解析表示元素类型的类或接口的符号引用。最后,对从
D到C的访问应用访问控制(§5.4.4)。
如果步骤1和2成功但步骤3失败,C仍然有效且可用。然而,解析失败,并且禁止D访问C。
5.4.3.2 字段解析
为了解析从D到类或接口C中的字段的未解析符号引用,必须首先解析对该字段引用给出的C的符号引用(§5.4.3.1)。因此,任何由于类或接口引用解析失败而可能抛出的异常都可以作为字段解析失败的结果抛出。如果对C的引用可以成功解析,则可以抛出与字段引用本身解析失败相关的异常。
解析字段引用时,字段解析首先尝试在C及其超类中查找所引用的字段:
如果
C声明了一个具有字段引用指定的名称和描述符的字段,则字段查找成功。声明的字段是字段查找的结果。否则,对
C的直接超接口递归应用字段查找。否则,如果
C有超类S,则对S递归应用字段查找。否则,字段查找失败。
然后,字段解析的结果确定如下:
- 如果字段查找失败,字段解析抛出
NoSuchFieldError。 - 否则,字段查找成功。对从
D到字段查找结果字段的访问应用访问控制(§5.4.4)。然后:- 如果访问控制失败,字段解析因相同原因失败。
- 否则,访问控制成功。施加加载约束,如下所示。
设<E, L1>是实际声明所引用字段的类或接口。设L2是D的定义加载器。给定所引用字段的类型为Tf:如果Tf不是数组类型,令T为Tf;否则,令T为Tf的元素类型。Java虚拟机施加加载约束TL1 = TL2。如果施加此约束导致任何加载约束被违反(§5.3.4),则字段解析失败。否则,字段解析成功。
5.4.3.3 方法解析
为了解析从D到类C中的方法的未解析符号引用,必须首先解析对该方法引用给出的C的符号引用(§5.4.3.1)。因此,任何由于类引用解析失败而可能抛出的异常都可以作为方法解析失败的结果抛出。如果对C的引用可以成功解析,则可以抛出与方法引用本身解析失败相关的异常。
解析方法引用时:
如果
C是一个接口,方法解析抛出IncompatibleClassChangeError。否则,方法解析尝试在
C及其超类中定位所引用的方法:- 如果
C声明了恰好一个具有方法引用指定名称的方法,并且该声明是签名多态方法(§2.9.3),则方法查找成功。描述符中提到的所有类名都被解析(§5.4.3.1)。解析的方法是签名多态方法声明。C不需要声明具有方法引用指定描述符的方法。 - 否则,如果
C声明了具有方法引用指定的名称和描述符的方法,则方法查找成功。 - 否则,如果
C有超类,则在C的直接超类上递归调用方法解析的第2步。
- 如果
否则,方法解析尝试在指定类
C的超接口中定位所引用的方法:- 如果
C的对于指定名称和描述符的最具体超接口方法(maximally-specific superinterface methods)中恰好包含一个未设置ACC_ABSTRACT标志的方法,则选择此方法,方法查找成功。 - 否则,如果
C的任何超接口声明了具有指定名称和描述符且既未设置ACC_PRIVATE标志也未设置ACC_STATIC标志的方法,则任意选择其中一个,方法查找成功。 - 否则,方法查找失败。
- 如果
类或接口C对于特定方法名称和描述符的最具体超接口方法是指满足以下所有条件的方法:
- 该方法声明在
C的(直接或间接)超接口中。 - 该方法使用指定的名称和描述符声明。
- 该方法既未设置
ACC_PRIVATE标志也未设置ACC_STATIC标志。 - 该方法声明在接口
I中,并且不存在另一个声明在I的子接口中的、具有相同名称和描述符的、更具体的超接口方法。
方法解析的结果确定如下:
- 如果方法查找失败,方法解析抛出
NoSuchMethodError。 - 否则,方法查找成功。对从
D到方法查找结果方法的访问应用访问控制(§5.4.4)。然后:- 如果访问控制失败,方法解析因相同原因失败。
- 否则,访问控制成功。施加加载约束,如下所示。
设<E, L1>是实际声明所引用方法m的类或接口。设L2是D的定义加载器。给定m的返回类型为Tr,m的形式参数类型为Tf1, ..., Tfn:
- 如果
Tr不是数组类型,令T0为Tr;否则,令T0为Tr的元素类型。 - 对于
i = 1到n:如果Tfi不是数组类型,令Ti为Tfi;否则,令Ti为Tfi的元素类型。 - 对于
i = 0到n,施加加载约束TiL1 = TiL2。
如果施加这些约束导致任何加载约束被违反(§5.3.4),则方法解析失败。否则,方法解析成功。
5.4.3.4 接口方法解析
为了解析从D到接口C中的接口方法的未解析符号引用,必须首先解析对该接口方法引用给出的C的符号引用(§5.4.3.1)。因此,任何由于接口引用解析失败而可能抛出的异常都可以作为接口方法解析失败的结果抛出。如果对C的引用可以成功解析,则可以抛出与接口方法引用本身解析失败相关的异常。
解析接口方法引用时:
如果
C不是接口,接口方法解析抛出IncompatibleClassChangeError。否则,如果
C声明了具有接口方法引用指定的名称和描述符的方法,方法查找成功。否则,如果类
Object声明了具有指定名称和描述符、设置了ACC_PUBLIC标志且未设置ACC_STATIC标志的方法,方法查找成功。否则,如果
C的对于指定名称和描述符的最具体超接口方法(§5.4.3.3)中恰好包含一个未设置ACC_ABSTRACT标志的方法,则选择此方法,方法查找成功。否则,如果
C的任何超接口声明了具有指定名称和描述符且既未设置ACC_PRIVATE标志也未设置ACC_STATIC标志的方法,则任意选择其中一个,方法查找成功。否则,方法查找失败。
接口方法解析的结果确定如下:
- 如果方法查找失败,接口方法解析抛出
NoSuchMethodError。 - 否则,方法查找成功。对从
D到方法查找结果方法的访问应用访问控制(§5.4.4)。然后:- 如果访问控制失败,接口方法解析因相同原因失败。
- 否则,访问控制成功。施加加载约束,方式同§5.4.3.3中方法解析的约束施加。如果违反任何加载约束,解析失败;否则成功。
5.4.3.5 方法类型与方法句柄解析
为了解析对方法类型的未解析符号引用,就好像解析对类和接口的未解析符号引用(§5.4.3.1),这些类和接口的名称对应于方法描述符(§4.3.3)中给出的类型。任何由于类引用解析失败而可能抛出的异常都可以作为方法类型解析失败的结果抛出。
成功的方法类型解析的结果是对表示该方法描述符的java.lang.invoke.MethodType实例的引用。
方法类型解析的发生与实际运行时常量池中是否包含方法描述符中指示的类和接口的符号引用无关。此外,解析被视为在未解析的符号引用上发生,因此解析一个方法类型的失败不一定会导致后续解析另一个具有相同文本方法描述符的方法类型失败,如果届时可以加载合适的类和接口的话。
解析对方法句柄的未解析符号引用更加复杂。Java虚拟机解析的每个方法句柄都有一个等效的指令序列,称为其字节码行为(bytecode behavior),由方法句柄的种类指示。九种方法句柄的整数值和描述如表5.4.3.5-A所示。
指令序列对字段或方法的符号引用表示为C.x:T,其中x和T是字段或方法的名称和描述符(§4.3.2、§4.3.3),C是找到该字段或方法的类或接口。
表5.4.3.5-A. 方法句柄的字节码行为
| 种类 | 描述 | 解释 |
|---|---|---|
| 1 | REF_getField | getfield C.f:T |
| 2 | REF_getStatic | getstatic C.f:T |
| 3 | REF_putField | putfield C.f:T |
| 4 | REF_putStatic | putstatic C.f:T |
| 5 | REF_invokeVirtual | invokevirtual C.m:(A*)T |
| 6 | REF_invokeStatic | invokestatic C.m:(A*)T |
| 7 | REF_invokeSpecial | invokespecial C.m:(A*)T |
| 8 | REF_newInvokeSpecial | new C; dup; invokespecial C.<init>:(A*)V |
| 9 | REF_invokeInterface | invokeinterface C.m:(A*)T |
设MH为正在解析的方法句柄的符号引用(§5.1)。同时:
- 设
R为MH中包含的字段或方法的符号引用。R从CONSTANT_MethodHandle的reference_index项引用的CONSTANT_Fieldref、CONSTANT_Methodref或CONSTANT_InterfaceMethodref结构派生而来。 - 设
T为R引用的字段的类型,或R引用的方法的返回类型。设A*为R引用的方法的参数类型序列(可能为空)。
为了解析MH,解析MH字节码行为中所有对类、接口、字段和方法的符号引用,使用以下四个步骤:
解析
R。当MH的字节码行为是种类1、2、3或4时,如同字段解析(§5.4.3.2);当是种类5、6、7或8时,如同方法解析(§5.4.3.3);当是种类9时,如同接口方法解析(§5.4.3.4)。解析
R的结果应用以下约束。这些约束对应于在验证或执行相关字节码行为的指令序列期间将强制执行的约束。- 如果
MH的字节码行为是种类8(REF_newInvokeSpecial),则R必须解析为在类C中声明的实例初始化方法。 - 如果
R解析为protected成员,则根据MH字节码行为的种类应用以下规则:- 对于种类1、3和5(
REF_getField、REF_putField和REF_invokeVirtual):如果C.f或C.m解析为protected字段或方法,且C与当前类处于不同的运行时包,则C必须可赋值给当前类。 - 对于种类8(
REF_newInvokeSpecial):如果C.<init>解析为protected方法,则C必须声明在与当前类相同的运行时包中。
- 对于种类1、3和5(
R必须根据MH字节码行为的种类解析为静态或非静态成员:- 对于种类1、3、5、7和9(
REF_getField、REF_putField、REF_invokeVirtual、REF_invokeSpecial和REF_invokeInterface):C.f或C.m必须解析为非静态字段或方法。 - 对于种类2、4和6(
REF_getStatic、REF_putStatic和REF_invokeStatic):C.f或C.m必须解析为静态字段或方法。
- 对于种类1、3、5、7和9(
- 如果
解析对类和接口的未解析符号引用,其名称对应于
A*中的每个类型,以及类型T,按此顺序。通过解析对方法类型的未解析符号引用来获取对
java.lang.invoke.MethodType实例的引用,该方法类型包含表5.4.3.5-B中为MH的种类指定的方法描述符。
表5.4.3.5-B. 方法句柄的方法描述符
| 种类 | 描述 | 方法描述符 |
|---|---|---|
| 1 | REF_getField | (C)T |
| 2 | REF_getStatic | ()T |
| 3 | REF_putField | (C,T)V |
| 4 | REF_putStatic | (T)V |
| 5 | REF_invokeVirtual | (C,A*)T |
| 6 | REF_invokeStatic | (A*)T |
| 7 | REF_invokeSpecial | (C,A*)T |
| 8 | REF_newInvokeSpecial | (A*)C |
| 9 | REF_invokeInterface | (C,A*)T |
在步骤1、3和4中,任何由于解析类、接口、字段或方法的符号引用失败而可能抛出的异常都可以作为方法句柄解析失败的结果抛出。在步骤2中,任何由于指定约束导致的失败都会导致方法句柄解析因IllegalAccessError而失败。
成功的方法句柄解析的结果是对表示方法句柄MH的java.lang.invoke.MethodHandle实例的引用。该java.lang.invoke.MethodHandle实例的类型描述符是上述方法句柄解析第三步中产生的java.lang.invoke.MethodType实例。
如果R引用的方法设置了ACC_VARARGS标志(§4.6),则java.lang.invoke.MethodHandle实例是可变元数方法句柄;否则是固定元数方法句柄。可变元数方法句柄在通过invoke调用时执行参数列表装箱(JLS §15.12.4.2),而其在invokeExact上的行为则如同未设置ACC_VARARGS标志。如果R引用的方法设置了ACC_VARARGS标志,并且A*为空序列或A*的最后一个参数类型不是数组类型,则方法句柄解析抛出IncompatibleClassChangeError,即创建可变元数方法句柄失败。
Java虚拟机实现不需要对方法类型或方法句柄进行驻留(intern)。即,结构相同但不同的符号引用可能不会分别解析为相同的java.lang.invoke.MethodType或java.lang.invoke.MethodHandle实例。
java.lang.invoke.MethodHandles类允许创建没有字节码行为的方法句柄,其行为由创建它们的方法定义。
5.4.3.6 动态计算常量与调用点解析
为了解析对动态计算常量或调用点的未解析符号引用R,有三个任务。首先,检查R以确定哪些代码将充当其引导方法,以及哪些参数将传递给该代码。其次,将参数打包成数组并调用引导方法。第三,验证引导方法的结果,并将其用作解析的结果。
第一个任务涉及以下步骤:
R给出对引导方法句柄的符号引用。引导方法句柄被解析(§5.4.3.5)以获得对java.lang.invoke.MethodHandle实例的引用。任何由于解析方法句柄符号引用失败而可能抛出的异常都可以在此步骤中抛出。- 如果
R是对动态计算常量的符号引用,则设D为引导方法句柄的类型描述符。D指示的第一个参数类型必须是java.lang.invoke.MethodHandles.Lookup,否则解析失败并抛出BootstrapMethodError。出于历史原因,对动态计算调用点的引导方法句柄没有类似约束。
- 如果
如果
R是对动态计算常量的符号引用,则它给出一个字段描述符。- 如果字段描述符指示原始类型,则获取表示该类型的预定义
Class对象的引用。 - 否则,字段描述符指示类、接口或数组类型。获取表示该类型的
Class对象的引用,如同解析对类或接口的未解析符号引用(§5.4.3.1)。 - 任何由于解析类或接口符号引用失败而可能抛出的异常都可以在此步骤中抛出。
- 如果字段描述符指示原始类型,则获取表示该类型的预定义
如果
R是对动态计算调用点的符号引用,则它给出一个方法描述符。获取对java.lang.invoke.MethodType实例的引用,如同解析对方法类型的未解析符号引用(§5.4.3.5),该类型具有与方法描述符相同的参数和返回类型。任何由于解析方法类型符号引用失败而可能抛出的异常都可以在此步骤中抛出。R给出零个或多个静态参数,它们向引导方法传递特定于应用程序的元数据。每个静态参数A按照R给出的顺序被解析,如下:- 如果
A是字符串常量,则获取对其String类实例的引用。 - 如果
A是数值常量,则通过以下过程获取对java.lang.invoke.MethodHandle实例的引用: a. 令v为数值常量的值,令T为与该数值常量类型对应的字段描述符。 b. 令MH为一个方法句柄,如同通过调用java.lang.invoke.MethodHandles的identity方法(以表示Object类的参数)产生。 c. 获取对java.lang.invoke.MethodHandle实例的引用,如同通过调用MH.invoke(v)(方法描述符为(T)Ljava/lang/Object;)产生。 - 如果
A是对动态计算常量的符号引用,其字段描述符指示原始类型T,则解析A,产生原始值v。给定v和T,按照上述数值常量的过程获取对java.lang.invoke.MethodHandle实例的引用。 - 如果
A是任何其他种类的符号引用,则结果是解析A的结果。
- 如果
在运行时常量池的符号引用中,对动态计算常量的符号引用是特殊的,因为它们可以通过BootstrapMethods属性(§4.7.23)在语法上引用自身。然而,Java虚拟机不支持解析依赖于自身的动态计算常量的符号引用(即,作为其自身引导方法的静态参数)。因此,当R和A都是对动态计算常量的符号引用时,如果A与R相同,或者A给出的静态参数(直接或间接)引用R,则解析失败并抛出StackOverflowError。与类初始化(§5.5)中允许未初始化类之间存在循环不同,解析不允许动态计算常量符号引用中的循环。如果解析的实现使用栈,则会自然发生StackOverflowError。如果不然,实现被要求检测循环,而不是无限循环或为动态计算常量返回默认值。
类似的循环可能出现在引导方法体引用当前正在解析的动态计算常量时。这对invokedynamic引导来说一直是可能的,并且不需要在解析中特殊处理;递归的invokeWithArguments调用自然会引发StackOverflowError。
任何由于解析符号引用失败而可能抛出的异常都可以在此步骤中抛出。
第二个任务,调用引导方法句柄,涉及以下步骤:
分配一个组件类型为
Object、长度为n+3的数组,其中n是R给出的静态参数数量(n ≥ 0)。- 数组的第0个组件设置为对
java.lang.invoke.MethodHandles.Lookup实例的引用,该实例用于R出现的类,如同通过调用java.lang.invoke.MethodHandles的lookup方法产生。 - 数组的第一个组件设置为对表示
N(R给出的非限定名称)的String实例的引用。 - 数组的第二个组件设置为之前为
R给出的字段描述符或方法描述符获取的Class或java.lang.invoke.MethodType实例的引用。 - 后续组件设置为从解析
R的静态参数(如果有)中获得的引用。这些引用在数组中的顺序与R给出的相应静态参数的顺序相同。
Java虚拟机实现可以跳过数组分配,并在不改变可观察行为的情况下直接将参数传递给引导方法。
- 数组的第0个组件设置为对
引导方法句柄被调用,如同通过调用
BMH.invokeWithArguments(args),其中BMH是引导方法句柄,args是上面分配的数组。- 由于
java.lang.invoke.MethodHandle的invokeWithArguments方法的行为,引导方法句柄的类型描述符不需要与参数的运行时类型完全匹配。例如,引导方法句柄的第二个参数类型(对应于上面数组第一个组件中给出的非限定名称)可以是Object而不是String。如果引导方法句柄是可变元数的,则部分或全部参数可能被收集到尾部数组参数中。 - 调用发生在试图解析此符号引用的线程中。如果有多个这样的线程,引导方法句柄可能被并发调用。访问全局应用程序数据的引导方法应采取通常的竞争条件预防措施。
- 如果调用因抛出
Error或其子类的实例而失败,则解析失败并抛出该异常。 - 如果调用因抛出不是
Error或其子类的异常而失败,则解析失败并抛出BootstrapMethodError,其原因为抛出的异常。 - 如果多个线程并发为此符号引用调用引导方法句柄,Java虚拟机选择一个调用的结果并将其对所有线程可见地安装。任何为此符号引用执行的其他引导方法被允许完成,但其结果被忽略。
- 由于
第三个任务,验证由引导方法句柄调用产生的引用o,如下:
如果
R是对动态计算常量的符号引用,则将o转换为T类型(R给出的字段描述符指示的类型)。o的转换如同通过调用MH.invoke(o)(方法描述符为(Ljava/lang/Object;)T)产生,其中MH是一个方法句柄,如同通过调用java.lang.invoke.MethodHandles的identity方法(以表示Object类的参数)产生。转换的结果是解析的结果。如果转换因抛出NullPointerException或ClassCastException而失败,则解析失败并抛出BootstrapMethodError。如果
R是对动态计算调用点的符号引用,则当o满足以下所有属性时,o是解析的结果:o不为null。o是java.lang.invoke.CallSite或其子类的实例。java.lang.invoke.CallSite的类型在语义上等同于R给出的方法描述符。- 如果
o不具备这些属性,则解析失败并抛出BootstrapMethodError。
上述许多步骤执行“如同通过调用”某些方法的计算。在每种情况下,调用行为由invokestatic和invokevirtual的规范详细给出。调用发生在试图解析符号引用R的线程和类中。然而,不需要在运行时常量池中出现相应的方法引用,不一定使用任何特定方法的操作数栈,并且不强制执行任何方法Code属性的max_stack项的值。
如果多个线程同时尝试解析R,引导方法可能被并发调用。因此,访问全局应用程序数据的引导方法必须采取预防竞争条件的措施。
5.4.4 访问控制
访问控制在解析期间(§5.4.3)应用,以确保允许对类、接口、字段或方法的引用。如果指定的类、接口、字段或方法对引用类或接口是可访问的,则访问控制成功。
一个类或接口C对类或接口D是可访问的,当且仅当以下条件之一为真:
C是public,并且与D在同一个运行时模块(§5.3.6)中。C是public,并且与D在不同的运行时模块中,且C的运行时模块被D的运行时模块读取,并且C的运行时模块将C的运行时包导出到D的运行时模块。C不是public,且C和D是同一个运行时包的成员。
如果C对D不可访问,则访问控制抛出IllegalAccessError。否则,访问控制成功。
一个字段或方法R对类或接口D是可访问的,当且仅当以下条件之一为真:
R是public。R是protected,并在类C中声明,且D是C的子类或C本身。此外,如果R不是static,则对R的符号引用必须包含对类T的符号引用,使得T是D的子类、D的超类或D本身。在验证D期间,要求即使T是D的超类,受保护字段访问或方法调用的目标引用也必须是D或其子类的实例(§4.10.1.8)。R是protected或具有默认访问权限(即既不是public也不是protected也不是private),并由与D在同一运行时包中的类声明。R是private,并由属于与D相同nest的类或接口C声明,根据下面的nestmate测试。
如果R对D不可访问,则访问控制抛出IllegalAccessError。否则,访问控制成功。
一个嵌套(nest)是一组允许相互访问其私有成员的类和接口。其中一个类或接口是嵌套宿主(nest host)。它使用NestMembers属性(§4.7.29)枚举属于该嵌套的类和接口。它们中的每一个又使用NestHost属性(§4.7.28)将其指定为嵌套宿主。缺少NestHost属性的类或接口属于由其自身托管的嵌套;如果它也缺少NestMembers属性,则该嵌套仅由该类或接口本身组成的单例。
Java虚拟机确定给定类或接口所属的嵌套(即该类或接口指定的嵌套宿主)作为访问控制的一部分,而不是在加载类或接口时。Java SE平台API的某些方法可能在访问控制之前确定给定类或接口所属的嵌套,在这种情况下,Java虚拟机在访问控制期间尊重该先前确定。
要确定类或接口C是否与类或接口D属于同一嵌套,应用嵌套成员测试(nestmate test)。当且仅当嵌套成员测试成功时,C和D属于同一嵌套。嵌套成员测试如下:
- 如果
C和D是同一个类或接口,则嵌套成员测试成功。 - 否则,按顺序执行以下步骤:
- 令
H为D的嵌套宿主(如果D的嵌套宿主先前已确定)。如果D的嵌套宿主先前未确定,则使用下面的算法确定,得到H。 - 令
H'为C的嵌套宿主(如果C的嵌套宿主先前已确定)。如果C的嵌套宿主先前未确定,则使用下面的算法确定,得到H'。 - 比较
H和H'。如果H和H'是同一个类或接口,则嵌套成员测试成功。否则,嵌套成员测试失败。
- 令
类或接口M的嵌套宿主确定如下:
- 如果
M缺少NestHost属性,则M是它自己的嵌套宿主。 - 否则,
M具有NestHost属性,其host_class_index项用作M运行时常量池中的索引。解析该索引处的符号引用(§5.4.3.1)。- 如果解析符号引用失败,则
M是它自己的嵌套宿主。由于类或接口解析失败而抛出的任何异常不会被重新抛出。 - 否则,解析符号引用成功。令
H为解析的类或接口。M的嵌套宿主由以下规则确定:- 如果以下任何条件为真,则
M是它自己的嵌套宿主:H与M不在同一个运行时包中。H缺少NestMembers属性。H有NestMembers属性,但其classes数组中没有条目引用名称为N(M的名称)的类或接口。
- 否则,
H是M的嵌套宿主。
- 如果以下任何条件为真,则
- 如果解析符号引用失败,则
5.4.5 方法覆盖
实例方法mC可以覆盖另一个实例方法mA,当且仅当满足以下所有条件:
mC与mA具有相同的名称和描述符。mC未标记ACC_PRIVATE。- 以下条件之一为真:
mA标记ACC_PUBLIC。mA标记ACC_PROTECTED。mA既未标记ACC_PUBLIC也未标记ACC_PROTECTED也未标记ACC_PRIVATE,并且要么(a)mA的声明与mC的声明出现在同一个运行时包中,要么(b)如果mA在类A中声明且mC在类C中声明,则存在一个在类B中声明的方法mB,使得C是B的子类且B是A的子类,并且mC可以覆盖mB且mB可以覆盖mA。
最后一种情况的(b)部分允许默认访问方法的“传递覆盖”。例如,给定以下在包P中的类声明:
java
public class A { void m() {} }
public class B extends A { public void m() {} }
public class C extends B { void m() {} }以及在不同包中的类声明:
java
public class D extends P.C { void m() {} }则:
B.m可以覆盖A.m。C.m可以覆盖B.m和A.m。D.m可以覆盖B.m以及传递地覆盖A.m,但不能覆盖C.m。
5.4.6 方法选择
在执行invokeinterface或invokevirtual指令期间,相对于(i)栈上对象的运行时类型和(ii)指令先前解析的方法,选择一个方法。相对于类或接口C和方法mR选择方法的规则如下:
如果
mR标记ACC_PRIVATE,则它是所选方法。否则,所选方法由以下查找过程确定:
- 如果
C包含一个可以覆盖mR(§5.4.5)的实例方法m的声明,则m是所选方法。 - 否则,如果
C有超类,则从C的直接超类开始,并继续沿着超类链向上搜索,直到找到方法或没有更多超类。如果找到方法,则它是所选方法。 - 否则,确定
C的最具体超接口方法(§5.4.3.3)。如果恰好有一个匹配mR的名称和描述符且不是抽象方法,则它是所选方法。在此步骤中选择的任何最具体超接口方法都可以覆盖mR;无需显式检查这一点。
- 如果
虽然C通常是类,但在准备期间(§5.4.2)应用这些规则时,C也可以是接口。
5.5 初始化
类或接口的初始化包括执行其类或接口初始化方法(§2.9.2)。
类或接口C只能在以下情况之一发生时被初始化:
- 执行任何引用
C的Java虚拟机指令new、getstatic、putstatic或invokestatic(§new、§getstatic、§putstatic、§invokestatic)。- 执行
new指令时,要初始化的类是指令引用的类。 - 执行
getstatic、putstatic或invokestatic指令时,要初始化的类或接口是声明已解析字段或方法的类或接口。
- 执行
- 第一次调用
java.lang.invoke.MethodHandle实例,该实例是方法句柄解析(§5.4.3.5)的结果,且方法句柄的种类为2(REF_getStatic)、4(REF_putStatic)、6(REF_invokeStatic)或8(REF_newInvokeSpecial)。- 这意味着引导方法的类在调用
invokedynamic指令(§invokedynamic)时被初始化,作为调用点说明符的持续解析的一部分。
- 这意味着引导方法的类在调用
- 调用类库中的某些反射方法(§2.12),例如
Class类或java.lang.reflect包中的方法。 - 如果
C是一个类,其某个子类的初始化。 - 如果
C是一个声明了非抽象、非静态方法的接口,则直接或间接实现C的类的初始化。 - 在Java虚拟机启动时(§5.2)将其指定为初始类或接口。
在初始化之前,类或接口必须被链接,即被验证、准备和可选地解析。
由于Java虚拟机是多线程的,类或接口的初始化需要仔细同步,因为其他线程可能同时在尝试初始化同一个类或接口。还存在在类或接口的初始化过程中递归请求初始化的可能性。Java虚拟机的实现负责通过以下过程处理同步和递归初始化。它假设Class对象已经被验证和准备,并且Class对象包含指示四种情况之一的状态:
- 此
Class对象已验证和准备但未初始化。 - 此
Class对象正在被某个特定线程初始化。 - 此
Class对象已完全初始化并准备就绪。 - 此
Class对象处于错误状态,可能是因为初始化尝试并失败。
对于每个类或接口C,存在一个唯一的初始化锁LC。从C到LC的映射由Java虚拟机实现自行决定。例如,LC可以是C的Class对象,或与该Class对象关联的监视器。初始化C的过程如下:
同步
C的初始化锁LC。这涉及等待当前线程能够获取LC。如果
C的Class对象指示C的初始化正在由其他线程进行,则释放LC并阻塞当前线程,直到通知该进行中的初始化已完成,然后重复此过程。线程中断状态不受初始化过程执行的影响。如果
C的Class对象指示C的初始化正在由当前线程进行,则这必须是初始化的递归请求。释放LC并正常完成。如果
C的Class对象指示C已经初始化,则无需进一步操作。释放LC并正常完成。如果
C的Class对象处于错误状态,则无法进行初始化。释放LC并抛出NoClassDefFoundError。否则,记录当前线程正在对
C的Class对象进行初始化,并释放LC。然后,按照字段在
ClassFile结构中出现的顺序,将C的每个final static字段初始化为其ConstantValue属性(§4.7.2)中的常量值。接下来,如果
C是类而不是接口,令SC为其超类,令SI1, ..., SIn为C的所有(直接或间接)声明了至少一个非抽象、非静态方法的超接口。超接口的顺序由对C直接实现的每个接口的超接口层次结构的递归枚举给出。对于C直接实现的每个接口I(按C的interfaces数组的顺序),枚举在返回I之前递归到I的超接口(按I的interfaces数组的顺序)。对于列表[ SC, SI1, ..., SIn ]中的每个S,如果S尚未初始化,则递归地对此S执行整个过程。如有必要,首先验证和准备S。如果S的初始化因抛出异常而突然完成,则获取LC,将C的Class对象标记为错误,通知所有等待线程,释放LC,并突然完成,抛出与初始化SC结果相同的异常。接下来,通过查询
C的定义加载器来确定是否对C启用断言。接下来,执行
C的类或接口初始化方法。如果类或接口初始化方法的执行正常完成,则获取
LC,将C的Class对象标记为完全初始化,通知所有等待线程,释放LC,并正常完成此过程。否则,类或接口初始化方法必然因抛出某个异常
E而突然完成。如果E的类不是Error或其子类,则创建一个以E为参数的ExceptionInInitializerError类的新实例,并在下一步中使用此对象代替E。如果由于OutOfMemoryError无法创建ExceptionInInitializerError的新实例,则在下一步中使用OutOfMemoryError对象代替E。获取
LC,将C的Class对象标记为错误,通知所有等待线程,释放LC,并以E或上一步确定的替代对象作为原因突然完成此过程。
Java虚拟机实现可以通过省略步骤1中的锁获取(和步骤4/5中的释放)来优化此过程,当它可以确定类的初始化已经完成时,前提是在Java内存模型(JLS §17.4.5)方面,如果执行优化,所有在获取锁时会存在的先行发生顺序仍然存在。
5.6 绑定本地方法实现
绑定(Binding)是将用Java编程语言以外的语言编写的实现本地方法的函数集成到Java虚拟机中以便其可以执行的过程。虽然此过程传统上称为链接,但本规范中使用了术语“绑定”以避免与Java虚拟机链接类或接口混淆。
5.7 Java虚拟机退出
当某个线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且退出或停止操作被安全管理器允许时,Java虚拟机退出。
此外,JNI(Java本机接口)规范描述了当使用JNI调用API加载和卸载Java虚拟机时Java虚拟机的终止。