Java 虚拟机会动态地加载、链接并初始化类和接口。加载的过程是找到具有特定名称的类或接口类型的二进制表示形式,并根据该二进制表示形式创建类或接口。链接的过程是将一个类或接口组合成 Java 虚拟机的运行状态,以便能够执行。类或接口的初始化包括执行类或接口的初始化方法 < clinit>(第 2.9 节)。
在本章中,第 5.1 节描述了 Java 虚拟机如何从类或接口的二进制表示形式中获取符号引用。第 5.2 节解释了加载、链接和初始化这些过程是如何首先由 Java 虚拟机启动的。第 5.3 节规定了类和接口的二进制表示形式是如何由类加载器加载的,以及类和接口是如何创建的。链接在第 5.4 节中进行描述。第 5.5 节详细说明了类和接口是如何初始化的。第 5.6 节介绍了绑定本地方法的概念。最后,第 5.7 节描述了 Java 虚拟机何时退出。
运行时常量池
Java 虚拟机会为每个类型维护一个常量池,这是一个运行时的数据结构,其功能与传统编程语言实现中的符号表类似。
类或接口的二进制表示形式中的常量池表用于在创建类或接口时构建运行时常量池。运行时常量池中的所有引用最初都是符号形式的。这个 在运行时常量池中的符号引用是根据类或接口的二进制表示形式中的结构来推导得出的,具体方式如下:
对类或接口的符号引用源自类或接口二进制表示形式中的一个“CONSTANT_Class_info”结构。这种引用以“Class.getName”方法返回的形式给出了类或接口的名称,即:
- 对于非数组类或接口,名称就是该类或接口的二进制名称。
- 对于 n 维数组类,名称以 n 次出现的 ASCII "[" 字符开头,随后是元素类型的表示形式:
- 如果元素类型是基本类型,则用相应的字段描述符表示。
- 否则,如果元素类型是引用类型,则用 ASCII "L" 字符开头,随后是元素类型的二进制名称和 ASCII ";" 字符。
在本章中,每当提及类或接口的名称时,都应理解为是“Class.getName”方法返回的形式。
- 对类或接口中字段的符号引用源自二进制表示形式中的一段“常量字段引用信息”结构。这种引用会给出字段的名称和描述符,以及指向该字段所在类或接口的符号引用。
- 对类中方法的符号引用源自二进制表示形式中的一段“常量方法引用信息”结构。这种引用会给出方法的名称和描述符,以及指向该方法所在类的符号引用。
- 对接口中方法的符号引用源自二进制表示形式中的一段“常量接口方法引用信息”结构。这种引用会给出接口方法的名称和描述符,以及指向该方法所在接口的符号引用。
- 对方法句柄的符号引用源自二进制表示形式中的一段“常量方法句柄信息”结构。这种引用会给出一个指向类中字段的符号引用。根据方法处理对象的类型,它可能是类或接口,也可能是类的方法或接口的方法。
- 对方法类型的符号引用源自于类或接口的二进制表示形式中的 CONSTANT_MethodType_info 结构。这种引用会给出一个方法描述符)。
- 对调用站点说明符的符号引用源自于类或接口的二进制表示形式中的 CONSTANT_InvokeDynamic_info 结构。这种引用会提供:
- 一个符号引用,指向方法句柄,该句柄将作为被调用动态指令(§invokedynamic)的启动方法;
- 一系列符号引用(指向类、方法类型和方法句柄)、字符串字面量以及运行时常量值,这些将作为启动方法的静态参数;
- 方法名和方法描述符。 此外,某些非符号引用的运行时值是从常量池表中的项目中导出的:
- 字符串字面量是对类 String 的实例的引用,它源自于类或接口的二进制表示形式中的 CONSTANT_String_info 结构。CONSTANT_String_info 结构给出了构成该字符串字面量的 Unicode 码点序列。Java 编程语言规定,相同的字符串常量(即包含相同代码点序列的常量)必须指向同一个 String 类的实例。此外,如果对任何字符串调用 String.intern 方法,其结果将是一个指向与该字符串作为常量出现时所返回的相同类实例的引用。因此, 以下表达式必须为真: ("a" + "b" + "c").intern() == "abc" 为了生成字符串字面值,Java 虚拟机会检查由 CONSTANT_String_info 结构所给出的代码点序列。
- 如果在某个 String 类的实例上调用过方法 String.intern 并且该实例包含的 Unicode 代码点序列与 CONSTANT_String_info 结构所给出的序列完全相同,那么字符串字面值的生成结果就是对该 String 类实例的引用。
- 否则,会创建一个新的 String 类实例,该实例包含由 CONSTANT_String_info 结构所给出的 Unicode 代码点序列;a 对该类实例的引用是通过字符串字面值推导得出的结果。最后, 调用了新 String 实例的内部构造方法。
运行时常量值是根据类或接口的二进制表示形式中的 CONSTANT_Integer_info、CONSTANT_Float_info、CONSTANT_Long_info 或 CONSTANT_Double_info 结构得出的。 请注意,CONSTANT_Float_info 结构表示的是 IEEE 754 单精度格式的值,而 CONSTANT_Double_info 结构表示的是 IEEE 754 双精度格式的值。因此,从这些结构中得出的运行时常量值必须分别是能够使用 IEEE 754 单精度和双精度格式表示的值。
类或接口的二进制表示形式中的常量池表中的其余结构——CONSTANT_NameAndType_info 和 CONSTANT_Utf8_info 结构——仅在推导类、接口、方法、字段、方法类型和方法句柄的符号引用,以及推导字符串字面量和调用站点说明符时间接使用。
Java虚拟机启动
Java 虚拟机启动时会创建一个初始类,其创建方式取决于具体实现,使用的是引导类加载器。然后,Java 虚拟机会链接这个初始类、对其进行初始化,并调用公共类方法 void main(String[])。对这个方法的调用会驱动后续的所有执行过程。构成 main 方法的 Java 虚拟机指令的执行可能会导致(并因此产生)其他类和接口的链接(以及相应的创建),以及额外方法的调用。
在 Java 虚拟机的实现中,初始类可以作为命令行参数提供。或者,该实现也可以提供一个初始类,该初始类会设置一个类加载器,而该加载器会加载应用程序。 只要与前一段给出的规范保持一致,其他初始类的选择也是可行的。
创建与加载
创建一个类或接口 C(用名称 N 表示)的过程是在 Java 虚拟机的方法区域中构建 C 的特定于实现的内部表示。类或接口的创建是由另一个类或接口 D 触发的,该接口通过其运行时常量池引用 C。
类或接口的创建也可能由 D 调用某些 Java SE 平台类库)中的方法来触发,例如反射。 如果 C 不是数组类,那么它将通过使用类加载器加载 C 的二进制表示形式(§4(类文件格式))来创建。数组类没有外部的二进制表示形式;它们是由 Java 虚拟机而不是由类加载器创建的。
有两种类型的类加载器: 由 Java 虚拟机提供的引导类加载器,以及用户自定义的类加载器。每个用户自定义的类加载器都是抽象类 ClassLoader 的子类的一个实例。应用程序使用用户自定义的类加载器来扩展 Java 虚拟机动态加载并从而创建类的方式。用户自定义的类加载器可用于创建源自用户定义来源的类。例如, 一个类可以通过网络下载、即时生成,或者从加密文件中提取出来。
类加载器 L 可以通过直接定义或委托给另一个类加载器的方式来创建类 C。如果 L 直接创建 C,我们称 L 定义了 C,或者等价地说,称 L 是 C 的定义加载器。
当一个类加载器委托给另一个类加载器时,发起加载的加载器不一定与完成加载并定义该类的加载器相同。如果 L 通过直接定义或委托来创建 C,我们称 L 启动了 C 的加载,或者等价地说,称 L 是 C 的启动加载器。
在运行时,一个类或接口不是仅由其名称决定的,而是由一对参数决定的:其二进制名称(§4.2.1)和其定义的类加载器。每个这样的类或接口都属于一个单独的运行时包。类或接口的运行时包由类或接口的包名和定义的类加载器决定。
Java 虚拟机使用以下三种方法之一来创建由 N 表示的类或接口 C:
- 如果 N 表示一个非数组类或接口,那么将使用以下两种方法之一来加载并从而创建 C:
- 如果 D 是由引导类加载器定义的,那么引导类加载器会启动对 C 的加载。
- 如果 D 是由用户自定义的类加载器定义的,那么正是该相同的用户自定义类加载器会启动对 C 的加载。
- 否则,N 表示一个数组类。数组类是由 Java 虚拟机直接创建的,而不是由类加载器创建的。不过,在创建数组类 C 的过程中,会使用 D 的定义类加载器。
如果在类加载过程中出现错误,那么就必须在程序中某个(直接或间接)使用正在加载的类或接口的位置抛出一个 LinkageError 类的子类实例。
如果 Java 虚拟机在验证或解析阶段(但不是初始化阶段)尝试加载类 C,并且用于启动加载 C 的类加载器抛出了一个 ClassNotFoundException 实例,那么 Java 虚拟机必须抛出一个 NoClassDefFoundError 实例,其原因就是那个 ClassNotFoundException 实例。 (这里有一个微妙之处是,递归类加载以加载超类的方式作为解析(步骤 3)的一部分来执行。因此,由于类加载器未能加载超类而导致的 ClassNotFoundException 必须被封装在一个 NoClassDefFoundError 中。) 一个行为良好的类加载器应具备三个特性:
- 给定相同的名称,一个好的类加载器应始终返回相同的 Class 对象。
- 如果类加载器 L1 将类 C 的加载委托给另一个加载器 L2,那么对于任何作为 C 的直接超类或直接超接口出现的类型 T,或者作为 C 的类型出现的类型 T,在 C 语言中,或者作为方法或构造函数的正式参数的类型,在 C 语言中作为方法的返回类型时,L1 和 L2 应该返回相同的类对象。
- 如果用户自定义的类加载器预取类和接口的二进制表示形式,或者将一组相关类一起加载,那么它必须仅在程序中可能出现加载错误的地方反映这些错误,而这些错误如果没有进行预取或分组加载是本不应出现的。
我们有时会使用符号 <N, Ld> 来表示一个类或接口,其中 N 表示类或接口的名称,Ld 表示该类或接口的定义加载器。
我们也会使用符号 N 来表示一个类或接口。 其中,N 表示类或接口的名称,而 Li 则表示该类或接口的启动加载器。
使用Bootstrap类加载器进行加载
以下步骤用于通过引导类加载器加载并创建由 N 表示的非数组类或接口 C。
首先,Java 虚拟机会判断引导类加载器是否已记录为由 N 表示的类或接口的启动加载器。如果是,则该类或接口即为 C,无需进行类创建操作。
否则,Java 虚拟机会将参数 N 传递给引导类加载器上的一个方法调用,以搜索 C 的拟议表示形式。 以依赖于平台的方式进行。通常,一个类或接口会通过层次化的文件系统中的一个文件来表示,并且该类或接口的名称会编码在文件的路径名中。
请注意,不能保证所找到的所谓表示是有效的,也不是 C 的表示形式。这个加载阶段必须检测以下错误:
- 如果未找到所谓的 C 的表示形式,加载会抛出一个实例化的 ClassNotFoundException 异常。
然后,Java 虚拟机尝试使用从所谓表示中通过第 5.3.5 节中找到的算法从引导类加载器中推导出由 N 表示的类。该类就是 C。
使用用户定义的类加载器进行加载
以下步骤用于加载并创建由标识符 N 表示的非数组类或接口 C,该类或接口是用户自定义的类加载器 L 所负责加载的。
首先,Java 虚拟机会判断加载器 L 是否已记录为标识符 N 所代表的类或接口的启动加载器。如果是,则该类或接口即为 C,无需创建新的类。 否则,Java 虚拟机会调用 L 的 loadClass(N) 方法。该调用返回的值即为创建的类或接口 C。然后,Java 虚拟机会记录加载器 L 是 C 的启动加载器。本节的其余部分将更详细地描述这一过程。
当加载器 L 被调用以使用标识符 N(即要加载的类或接口 C 的名称)执行 loadClass 方法时,加载器 L 必须执行以下两种操作之一来加载 C:
- 类加载器 L 可以创建一个表示 C 的字节数组,该数组代表了 ClassFile 结构的字节内容(见第 4.1 节);然后它必须调用类 ClassLoader 的 defineClass 方法。调用 defineClass 会使 Java 虚拟机使用 L 从该字节数组中根据第 5.3.5 节中所描述的算法来推导出由 N 表示的类或接口。
- 类加载器 L 可以将 C 的加载任务委托给其他类加载器 L'。 这通过将参数 N 直接或间接传递给 L' 上某个方法的调用来实现(通常是 loadClass 方法)。调用的结果就是 C。 在(1)或(2)中,如果类加载器 L 因任何原因无法加载由 N 表示的类或接口,它必须抛出一个 ClassNotFoundException 的实例。
自 JDK 1.1 版本发布以来,Oracle 的 Java 虚拟机实现会调用类加载器的 loadClass 方法,以促使它加载一个类或接口。 loadClass 方法的参数是要加载的类或接口的名称。“有” 此外,还有一种带有两个参数的“loadClass”方法,其中第二个参数是一个布尔值,用于指示是否要链接该类或接口。在 JDK 1.0.2 版本中,仅提供了这种带有两个参数的版本,而甲骨文的 Java 虚拟机实现则依赖于它来链接加载的类或接口。从 JDK 1.1 版本开始,甲骨文的 Java 虚拟机实现直接链接该类或接口,不再依赖于类加载器。
创建数组类
以下步骤用于使用类加载器 L 创建具有标识符 N 的数组类 C 。类加载器 L 可以是引导类加载器,也可以是用户自定义的类加载器。
如果 L 已经被记录为具有与 N 相同组件类型的数组类的启动加载器,则该类即为 C ,无需创建数组类。 否则,将执行以下步骤来创建 C :1. 如果组件类型为引用类型,那么本节所述的算法(第 5.3 节)将通过使用类加载器 L 递归执行,以加载并从而创建 C 的组件类型。2. Java 虚拟机会创建一个具有指定元素类型和维度数量的新数组类。
如果元素类型是引用类型,那么 C 将被标记为由该元素类型的定义类加载器所定义。否则,C 将被标记为由引导类加载器所定义。
无论如何,Java 虚拟机随后会记录 L 是 C 的启动加载器。 如果元素类型是引用类型,数组类的可访问性将由其元素类型的可访问性来决定。否则的话, 数组类的可访问性为公有。
加载约束条件
在存在类加载器的情况下确保类型安全的链接需要格外小心谨慎。它是 有可能的是,当两个不同的类加载器分别开始加载由 N 表示的类或接口时, 名称 N 在每个加载器中可能代表不同的类或接口。
当类或接口 C = <N1, L1> 对另一个类或接口 D = <N2, L2> 的字段或方法进行符号引用时, 该符号引用包含一个描述符,用于指定字段的类型,或者方法的返回类型和参数类型。必须确保在由 L1 加载和由 L2 加载时,符号引用中提到的任何类型名称 N 都表示相同的类或接口。 为了实现这一点,Java 虚拟机实施了以下形式的加载约束:N L1 = NL2 在准备阶段(§5.4.2)和解决阶段(§5.4.3)中。为了执行这些约束条件,Java 虚拟机会在特定的时间点(见 §5.3.1、§5.3.2、§5.3.3 和 §5.3.5)记录某个加载器是某个类的启动加载器。在记录某个加载器是某个类的启动加载器之后,Java 虚拟机必须立即检查是否有任何加载约束条件被违反。如果是这样,该记录将被撤销,Java 虚拟机将抛出一个“链接错误”,并且导致该记录发生的加载操作将失败。
同样,在施加加载约束条件(见 §5.4.2、§5.4.3.2、§5.4.3.3 和 §5.4.3.4)之后,Java 虚拟机必须立即检查是否有任何加载约束条件被违反。如果是这样,新施加的加载约束条件将被撤销,Java 虚拟机将抛出一个“链接错误”,并且导致该约束条件被施加的操作(无论是解决还是准备,视情况而定)将失败。 这里描述的情况是 Java 虚拟机检查是否有任何加载约束条件被违反的唯一时间点。加载约束条件是……只有当以下四个条件同时满足时,该条件才成立:
- 存在一个加载器 L,使得该加载器已被 Java 虚拟机记录为名为 N 的类 C 的启动加载器。
- 存在另一个加载器 L',使得该加载器已被 Java 虚拟机记录为名为 N' 的类 C' 的启动加载器。
- 由所施加的约束集的(传递闭包)所定义的等价关系意味着 N 。l = N“L’” .
- C ≠ C '。 关于类加载器和类型安全性的详细讨论超出了本规范的范围。 如需更全面的讨论,读者可参考盛良和吉拉德·布拉查所著的《Java 虚拟机中的动态类加载》(1998 年美国计算机协会面向对象编程系统、语言与体系结构的会议论文集)。(应用)
从类文件表示形式派生一个类
以下步骤用于根据以类文件格式呈现的所谓表示内容,使用加载器 L 为非数组类或接口 C(用符号 N 表示)生成一个类对象。
-
首先,Java 虚拟机会判断它是否已经记录了 L 是由 N 表示的类或接口的启动加载器这一信息。如果是这样,那么此次创建尝试就是无效的,加载过程会抛出一个“链接错误”。
-
否则,Java 虚拟机会尝试解析这个声称的内容。代理人。然而,所声称的表示形式实际上可能并非 C 的有效表示形式。 此加载阶段必须检测以下错误:
- 如果所声称的表示形式不是类文件结构(§4.1,§4.8),则加载会抛出一个 ClassFormatError 的实例。
- 否则,如果所声称的表示形式不是支持的主版本或次版本(§4.1),则加载会抛出一个 UnsupportedClassVersionError 的实例。 UnsupportedClassVersionError 是 ClassFormatError 的一个子类,其目的是为了便于识别由于尝试加载其表示形式使用了不支持的类文件格式的类而导致的 ClassFormatError。在 JDK 1.1 及更早版本中,如果使用的是不支持的版本,会根据类是被系统类加载器还是用户定义的类加载器加载而抛出 NoClassDefFoundError 或 ClassFormatError 的实例。
- 否则,如果所声称的表示形式实际上并不表示名为 N 的类,则加载会抛出一个 NoClassDefFoundError 的实例或其子类的一个实例。
-
如果 C 具有直接父类,则从 C 到其直接父类的符号引用将按照第 5.4.3.1 节中的算法进行解析。请注意,如果 C 是一个接口,那么它必须具有 Object 作为其直接父类,而 Object 本身必须已经加载完成。 只有 Object 没有直接父类。 由于类或接口解析而可能抛出的任何异常都可能由于此加载阶段而被抛出。此外,此加载阶段必须检测以下错误:
- 如果作为 C 的直接父类命名的类或接口实际上是另一个接口,则加载会抛出 IncompatibleClassChangeError 错误。
- 否则,如果 C 的任何父类都是 C 本身,则加载会抛出 ClassCircularityError 错误。
-
如果 C 有任何直接的超接口,那么从 C 到其直接超接口的符号引用将按照第 5.4.3.1 节中的算法进行解析。 由于类或接口解析而可能引发的任何异常都可能在这一加载阶段被抛出。此外,这一加载阶段必须检测以下错误:
- 如果作为 C 的直接超接口所命名的任何类或接口实际上不是接口,那么加载将抛出一个“不兼容类变更错误”。
- 否则,如果 C 的任何超接口是 C 本身,那么加载将抛出一个“类循环错误”。
-
Java 虚拟机将 C 的定义类加载器标记为 L,并记录 L 是 C 的启动加载器(§5.3.4)。
链接
将一个类或接口链接起来需要验证并准备该类或接口、其直接父类、其直接父接口以及其元素类型(如果它是数组类型的话)。在类或接口中解析符号引用是链接过程中的一个可选部分。 本规范允许实现者在何时进行链接活动(以及由于递归而进行的加载)方面具有灵活性,但前提是必须保持以下所有属性:
- 在链接之前,类或接口必须完全加载。
- 在初始化之前,类或接口必须完全验证并准备就绪。
- 在链接过程中检测到的错误应在程序中某个由程序采取的、可能直接或间接需要链接到涉及错误的类或接口的操作点处抛出。 例如,Java 虚拟机实现可能选择在使用类或接口时单独解析每个符号引用(“延迟”或“后期”解析),或者在验证类时一次性解析所有符号引用(“早期”或“静态”解析)。这意味着解析过程可能在某些实现方式中,在类或接口初始化完成后会继续执行。 无论采用哪种策略,解析过程中检测到的任何错误都必须在程序中(直接或间接)使用该类或接口的符号引用的某个位置抛出。
因为链接涉及新数据结构的分配,所以它可能会因“内存不足错误”而失败。
验证
验证确保类或接口的二进制表示形式在结构上是正确的。验证可能会导致其他类和接口被加载,但无需对它们进行验证或准备。
如果类或接口的二进制表示形式不符合第节中列出的静态或结构约束,则在导致该类或接口进行验证的程序中的相应位置必须抛出一个“验证错误”。
如果 Java 虚拟机尝试验证一个类或接口的尝试因抛出的错误是“链接错误”(或其子类)而失败,则后续尝试验证该类或接口总是会因与最初验证尝试所抛出的相同错误而失败。
准备
准备工作包括为类或接口创建静态字段,并将这些字段初始化为其默认值。此过程无需执行任何 Java 虚拟机代码;静态字段的显式初始化是作为初始化的一部分执行的,而非准备工作的一部分。
在准备类或接口 C 时,Java 虚拟机还会施加加载约束。设 L1 为 C 的定义加载器。对于 C 中声明的每个重写了超类或超接口 <D,L2> 中声明的方法 m 的方法 m,Java 虚拟机会施加以下加载约束: 假设 m 的返回类型为 Tr,且 m 的形式参数类型为 Tf1 至 Tfn,则:
如果 Tr 不是数组类型,设 T0 为 Tr;否则,设 T0 为 Tr 的元素类型。 对于 i = 1 到 n:如果 Tfi 不是数组类型,设 Ti 为 Tfi;否则,设 Ti 为 Tfi 的元素类型。 然后 TiL1 = 塔伊L2 对于 i 的取值范围为 0 到 n: 加载、链接以及初始化 链接 5.4345
此外,如果 C 实现了一个在 C 的超接口 <I, L3> 中声明的方法 m,但 C 自身并未声明该方法 m,那么令 <D, L2> 为 C 的父类,该父类声明了 C 所继承的方法 m 的实现。Java 虚拟机规定了以下约束条件: 假设 m 的返回类型为 Tr,且 m 的形式参数类型为 Tf1、...、Tfn,那么: 如果 Tr 不是数组类型,令 T0 为 Tr;否则,令 T0 为 Tr 的元素类型(§2.4)。 对于 i = 1 到 n:如果 Tfi 不是数组类型,令 Ti 为 Tfi;否则,令 Ti 为 Tfi 的元素类型(§2.4)。 那么 TiL2 = 塔伊L3 对于 i 从 0 到 n 的每一项: 在创建之后的任何时间都可以进行准备工作,但必须在初始化之前完成。
解析
Java 虚拟机的指令如 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield 和 putstatic 都会指向运行时常量池中的符号引用。执行这些指令中的任何一条都需要对其中的符号引用进行解析。
解析是动态地从运行时常量池中的符号引用中确定具体值的过程。 对一个 invokedynamic 指令的某个出现位置的符号引用进行解析并不意味着该相同的符号引用对于任何其他 invokedynamic 指令都被视为已解析。
对于上述所有其他指令,一个指令的某个出现位置的符号引用的解析意味着该相同的符号引用对于任何其他非 invokedynamic 指令都被视为已解析。 (上述文本暗示,由解析确定的特定 invokedynamic 指令的具体值是一个绑定到该特定 invokedynamic 指令的调用站点对象。)
可以对已经存在的符号引用尝试进行解析。下定决心的。尝试解决一个已经成功解决过的符号引用总是会简单地成功完成,并且总是会生成与该引用最初解决时相同的实体。 如果在解决符号引用的过程中出现错误,那么在程序中(直接或间接)使用该符号引用的某个点就必须抛出一个“不兼容类变更错误”(或其子类)的实例。
如果 Java 虚拟机尝试解决一个符号引用时由于抛出的错误是“链接错误”(或其子类)而失败,那么后续尝试解决该引用时总是会以最初解决尝试所导致抛出的相同错误而失败。
由特定的“调用动态”指令指向的调用点说明符的符号引用必须在该指令执行之前不进行解决。 对于“调用动态”指令的解决失败情况,后续的解决尝试不会在再次尝试时重新执行引导方法。 上述某些指令在解决符号引用时需要进行额外的链接检查。例如,为了使“获取字段”指令能够成功执行,就需要进行这样的检查。要解析其操作所依赖的字段的符号引用,它不仅必须 完成第 5.4.3.2 节中给出的字段解析步骤,而且还必须检查该字段是否为静态字段。如果是静态字段,则必须抛出链接异常。
值得注意的是,为了使调用动态指令能够成功解析到调用站点说明符的符号引用,其中指定的引导方法必须正常完成并返回一个合适的调用站点对象。如果引导方法突然完成或返回一个不合适的调用站点对象,则必须抛出链接异常。
由特定于某一特定 Java 虚拟机指令的执行而产生的检查所生成的链接异常在该指令的描述中有所说明,并不在这一关于解析的通用讨论中涵盖。 注意 这些例外情况尽管被描述为是执行 Java 虚拟机指令的一部分,而非解决过程的一部分,但仍应被视为解决过程中的失败情况。
以下各节描述了在类或接口 D 的运行时常量池中解析符号引用的过程。解决过程的细节会因要解析的符号引用的类型而有所不同。
类和接口解析
为了解决从 D 到由 N 表示的类或接口 C 的未解决的符号引用问题,需执行以下步骤:
- D 的定义性类加载器用于创建由 N 表示的类或接口。这个类或接口就是 C。该过程的详#细信息见第 5.3 节。 任何因类或接口创建失败而可能抛出的异常,因此也会因类和接口的创建失败而被抛出。决议。
- 如果 C 是一个数组类,且其元素类型为引用类型,那么对表示该元素类型的类或接口的符号引用将通过递归调用第 5.4.3.1 节中的算法来解析。
- 最后,会检查对 C 的访问权限。
- 如果 D 无法访问 C(参见 5.4.4 节),则类或接口解析会抛出一个“非法访问错误”。 这种情况可能会出现,例如,如果 C 是一个原本声明为公共的类,但在 D 编译后被修改为非公共的。
如果步骤 1 和 2 成功,但步骤 3 失败,那么 C 仍然是有效的且可用的。然而,解析失败了,D 被禁止访问 C。
字段解析
为了解决从 D 到类或接口 C 中某个字段的未解决的符号引用问题 ,由字段引用给出的指向 C 的符号引用必须首先得到解析。因此,任何因类或接口引用解析失败而可能抛出的异常,都可以因字段解析失败而被抛出。如果对 C 的引用能够成功解析,那么就可以抛出与字段引用解析失败本身相关的异常。
在解析字段引用时,字段解析首先会尝试在 C 及其超类中查找被引用的字段:
- 如果 C 使用由字段引用指定的名称和描述符来声明一个字段,那么字段查找就会成功。所声明的字段即为字段查找的结果。
- 否则,字段查找会递归地应用于指定类或接口 C 的直接超类。
- 否则,如果 C 具有超类 S,那么字段查找将递归地应用于 S 。
- 否则,字段查找将失败。然后:
- 若字段查找失败,则字段解析会抛出“未找到字段”错误。
- 4 连接“加载”、“链接”和“初始化”操作348
- 另一方面,如果字段查找成功,但所引用的字段对 D 来说不可访问(§5.4.4),则字段解析会抛出非法访问错误。
- 另外,设 <E, L1> 为实际声明所引用字段的类或接口,并设 L2 为 D 的定义加载器。 鉴于所引用字段的类型为 Tf,若 Tf 不是数组类型,则设 T 为 Tf,否则设 T 为 Tf 的元素类型(§2.4)。 Java 虚拟机必须施加加载约束条件,即 TL1 = TL2
方法解析
若要解决从 D 到类 C 中某个方法的未解决的符号引用问题,首先会解析该方法引用所给出的指向类 C 的符号引用。
因此,任何因类引用解析失败而可能引发的异常,也都可以因方法解析失败而引发。如果 对于 C 的引用能够成功解析,但与方法引用的解析本身相关的异常可能会被抛出。 在解析方法引用时:
- 如果 C 是一个接口,那么方法解析就会引发“不兼容类变更错误”。
- 否则,方法解析会尝试在 C 类及其所有父类中查找所引用的方法:
- 如果 C 类恰好声明了一个具有指定方法引用名称的方法,并且该声明是签名多态方法的话,那么方法查找就成功了。描述符中提到的所有类名都会被解析。 解析后的方法就是签名多态方法声明。它是 对于 C 语言而言,无需使用由方法引用指定的描述符来声明方法。
- 另外,如果 C 语言声明了一个具有由方法引用指定的名称和描述符的方法,那么方法查找就会成功。
- 另外,如果 C 语言有一个父类,那么在方法解析的步骤 2 中,会递归地对 C 的直接父类进行调用。
- 否则,方法解析会尝试在指定类 C 的超接口中查找所引用的方法:
- 如果 C 的最具体超接口中针对指定方法名和描述符的那些方法恰好只包含一个未设置 ACC_ABSTRACT 标志的方法,则选择该方法并方法查找成功。
- 否则,如果 C 的任何超接口声明了一个具有指定方法名和描述符且既未设置 ACC_PRIVATE 标志也未设置 ACC_STATIC 标志的方法,则随机选择其中一个方法并方法查找成功。
- 否则,方法查找失败。
一个类或接口 C 对于特定方法名和描述符的最具体超接口方法是任何满足以下条件的方法:
- 该方法在 C 的超接口(直接或间接)中声明。
- 该方法具有指定的方法名和描述符。
- 该方法既未设置 ACC_PRIVATE 标志也未设置 ACC_STATIC 标志。
- 如果该方法在接口 I 中声明,则不存在具有指定方法名和描述符且为最具体超接口方法的 C 的其他接口。在 I 的子接口中进行声明。 方法解析的结果取决于方法查找是否成功或失败:
- 如果方法查找失败,则方法解析会抛出 NoSuchMethodError 异常。
- 否则,如果方法查找成功且所引用的方法对 D 不可访问(§5.4.4),则方法解析会抛出 IllegalAccessError 异常。
- 否则,令 <E, L1> 为所引用方法 m 实际声明所在的类或接口,并令 L2 为 D 的定义加载器。
鉴于 m 的返回类型为 Tr,且 m 的形式参数类型为 Tf1、...、Tfn,则:
如果 Tr 不是数组类型,则令 T0 为 Tr;否则,令 T0 为 Tr 的元素类型(§2.4)。 对于 i = 1 到 n:如果 Tfi 不是数组类型,则令 Ti 为 Tfi;否则,令 Ti 为 Tfi 的元素类型(§2.4)。 Java 虚拟机必须施加加载约束条件 TiL1 = Ti L2为了 对于 i 的取值范围为 0 到 n(见 5.3.4 节)的情况。
当解析器在类的超接口中查找方法时,最佳结果是找到一个尽可能具体的非抽象方法。有可能这个方法会被方法选择机制所选用,因此有必要为其添加类加载器约束。否则,结果将是不确定的。这并非新问题:Java®虚拟机规范从未明确指出会选择哪个方法,也未说明“平局”情况应如何处理。在 Java SE 8 之前,这基本上是一个难以察觉的差异。然而, 从 Java SE 8 开始,接口方法的集合变得更加多样化,因此必须谨慎处理以避免出现非确定性行为方面的问题。因此:
- 私有且静态的超接口方法在解析过程中会被忽略。这是 与 Java 编程语言的规定一致,在该语言中此类接口方法不会被继承。
- 由已解析的方法所控制的任何行为都不应取决于该方法是否为抽象方法。 请注意,如果解析的结果是抽象方法,被引用的类 C 可能并非抽象类。要求 C 为抽象类将会与超接口方法的随机选择选择产生冲突。相反,解析假定调用对象的运行时类具有该方法的具体实现。
接口方法解析
若要解决从 D 到接口 C 中的一个接口方法的未解决的符号引用问题,首先会解析由接口方法引用所给出的对 C 的符号引用。因此,任何因接口引用解析失败而可能抛出的异常,也都可以因接口方法解析失败而被抛出。如果对 C 的引用能够成功解析,那么与接口方法引用的解析本身有关的异常就可以被抛出。“thrown”(被扔出) 在解析接口方法引用时:
-
如果 C 不是接口,那么接口方法解析就会引发“不兼容类变更错误”。
-
否则,如果 C 宣告了一个具有由接口方法引用所指定的名称和描述符的方法,那么方法查找就会成功。3. 否则,如果类“Object”中声明了一个具有由接口方法引用所指定的名称和描述符的方法,该方法的“ACC_PUBLIC”标志已设置且“ACC_STATIC”标志未设置,则方法查找将成功完成。
-
否则,如果 C 中针对由方法引用指定的名称和描述符所定义的最具体超接口方法中恰好包含一个未设置 ACC_ABSTRACT 标志的方法,那么将选择该方法,方法查找操作即成功完成。 加载、链接与初始化 链接 5.4351
-
否则,如果 C 类的任何超接口声明了一个具有由方法引用指定的名称和参数描述符的方法,且该方法既未设置 ACC_PRIVATE 标志也未设置 ACC_STATIC 标志,那么会随机选择其中一个,并且方法查找操作将成功完成。
-
否则,方法查找就会失败。 接口方法解析的结果取决于方法查找是否成功或失败:
- 如果方法查找失败,接口方法解析会抛出一个“NoSuchMethodError”异常。
- 如果方法查找成功但所引用的方法对 D 来说不可访问(§5.4.4),接口方法解析会抛出一个“IllegalAccessError”异常。
- 否则,令 <E, L1> 为实际声明所引用接口方法 m 的类或接口,并令 L2 为 D 的定义加载器。 鉴于 m 的返回类型为 Tr,且 m 的形式参数类型为 Tf1 至 Tfn,那么: 如果 Tr 不是数组类型,令 T0 为 Tr;否则,令 T0 为 Tr 的元素类型(§2.4)。 对于 i = 1 到 n:如果 Tfi 不是数组类型,令 Ti 为 Tfi;否则,令 Ti 为 Tfi 的元素类型(§2.4)。 Java 虚拟机必须施加加载约束条件 TiL1 = 塔伊L2 为了 i = 0 到 n (§5.3.4)。
关于可访问性的这一条款是必要的,因为接口方法的解析可能会选择接口 C 的私有方法。(在 Java SE 8 之前,接口方法解析的结果可能是类 Object 的非私有方法或者类 Object 的静态方法;)这样的 结果与 Java 编程语言的继承模型并不一致,并且在 Java SE 8 及更高版本中是被禁止使用的。
方法类型和方法句柄解析
要解决对方法类型的未解决符号引用问题,这就好比是解决未解决的类和接口的符号引用问题,而这些类和接口的名称与方法描述符中给出的类型相对应。
因此,由于类引用解析失败而可能抛出的任何异常,现在也可以因方法类型解析失败而被抛出。 成功完成方法类型解析的结果是引用一个表示方法描述符的 java.lang.invoke.MethodType 的实例。 方法类型解析与运行时常量池中是否实际包含方法描述符中所指示的类和接口的符号引用无关。
此外,这种解析被视为对未解决的符号引用进行的,所以如果后续时间能够加载合适的类和接口,那么一个方法类型的解析失败并不一定会导致另一个具有相同文本方法描述符的方法类型的解析失败。
对未解决的符号方法指针的解析则更为复杂。复杂的。由 Java 虚拟机解析的每个方法句柄都有一个与其对应的指令序列,称为其字节码行为,该行为由方法句柄的类型来表示。表 5.4.3.5-A 给出了九种方法句柄的整数值和描述。指令序列对字段或方法的符号引用表示为 C.x:T,其中 x 和 T 是字段或方法的名称和描述符,而 C 是要查找该字段或方法的类或接口。
设“MH”为用于标识正在被解析的方法句柄的符号引用。然后:
- 设 R 为 MH 内部所包含的字段或方法的符号引用(R 源自由 CONSTANT_Fieldref、CONSTANT_Methodref 或 CONSTANT_InterfaceMethodref 结构导出的引用索引项所指向的 CONSTANT_MethodHandle 结构中的 CONSTANT_NameAndType 结构)。
- 设 T 为由 R 引用的字段的类型,或由 R 引用的方法的返回类型。设 A* 为由 R 引用的方法的参数类型序列(可能为空)。
(T 和 A* 源自由 CONSTANT_NameAndType 结构导出的结构,该结构由 CONSTANT_Fieldref、CONSTANT_Methodref 或 CONSTANT_InterfaceMethodref 结构中的 name_and_type_index 项所指向。) 为解析 MH,对 MH 字节码行为中所有关于类、接口、字段和方法的符号引用进行解析,使用以下三个步骤:
- 首先,解析 R。
- 其次,如同对名称与 A* 中的每个类型以及类型 T 相对应且未解析的类和接口的符号引用进行解析一样,进行解析操作。订单。
- 第三,会获取到一个指向
java.lang.invoke.MethodType类型实例的引用,其获取方式类似于对一个未解析的表示方法类型的符号引用的解析过程,该引用包含了表 5.4.3.5-B 中针对特定 MH 类型所指定的方法描述符。就好像对方法柄的符号引用包含了最终被解析的方法柄所具有的方法类型的符号引用一样。方法类型的详细结构是通过查看表 5.4.3.5-B 获得的。
在每个步骤中,任何因类、接口、字段或方法引用解析失败而可能抛出的异常,都可以因方法柄解析失败而被抛出。
其目的是,解析方法柄的操作可以在与 Java 虚拟机成功解析字节码行为中的符号引用的环境完全相同的条件下进行。特别是,对私有和受保护成员的方法柄可以在对应于这些成员的合法正常访问的那些类中创建。
成功解析方法句柄的结果是一个指向 java.lang.invoke.MethodHandle 实例的引用,该实例代表方法句柄 MH。 此 java.lang.invoke.MethodHandle 实例的类型描述符是上述方法句柄解析步骤中的生成的 java.lang.invoke.MethodType 实例。
方法句柄的类型描述符是这样的:在 java.lang.invoke.MethodHandle 上对方法句柄执行 invokeExact 的有效调用具有与字节码行为完全相同的栈效应。在给定一组有效的参数上调用此方法句柄具有与相应的字节码完全相同的效果和返回值(如果有)。
如果 R 所引用的方法设置了 ACC_VARARGS 标志,那么该 java.lang.invoke.MethodHandle 实例就是一个可变参数方法句柄; 否则,它就是一个固定参数方法句柄。 一个可变参数方法句柄在通过 invoke 调用时会执行参数列表装箱(JLS 第 15.12.4.2 节),而其对于 invokeExact 的行为则如同 ACC_VARARGS 标志未被设置一样。 如果 R 所引用的方法设置了 ACC_VARARGS 标志,并且 A* 为空序列或者 A* 中的最后一个参数类型不是数组类型,那么方法句柄解析会抛出 IncompatibleClassChangeError 错误。也就是说,创建一个可变参数方法句柄会失败。
Java 虚拟机的实现无需对方法类型或方法句柄进行内部化处理。也就是说,两个结构完全相同的但具有不同符号引用的 方法类型或方法句柄是可能存在的。
不会与同一个实例的 java.lang.invoke.MethodType 或 java.lang.invoke.MethodHandle 相匹配。 Java SE 平台 API 中的 java.lang.invoke.MethodHandles 类允许创建具有无字节码行为的方法处理程序。它们的行为由创建它们的 java.lang.invoke.MethodHandles 方法来定义。例如,一个方法处理程序在被调用时,可能会首先对其参数值进行转换,然后将转换后的值提供给另一个方法处理程序的调用,然后对该调用返回的值进行转换,最后将转换后的值作为其自身的结果返回。
调用点指定符解析
要解决对调用点说明符的未解决符号引用问题,需要进行以下三个步骤。步骤:
- 调用点说明符会给出一个指向方法句柄的符号引用,该方法句柄将作为动态调用点的启动方法(§4.7.23)。 该方法句柄会被解析以获取对 java.lang.invoke.MethodHandle 类型实例的引用(§5.4.3.5)。
- 调用点说明符给出一个方法描述符 TD。通过解析一个符号引用(其参数和返回类型与 TD 相同)来获取一个指向 java.lang.invoke.MethodType 类型实例的引用(§5.4.3.5)。
- 调用点说明符给出零个或多个静态参数,这些参数会向启动方法传递应用程序特定的元数据。任何作为类、方法句柄或方法类型符号引用的静态参数都会被解析(如同调用 ldc 指令一样)以获取分别对应于 Class 对象、java.lang.invoke.MethodHandle 对象和 java.lang.invoke.MethodType 对象的引用。任何作为字符串字面量的静态参数都会用于获取字符串对象的引用。 调用点说明符的解析结果是一个由以下元素组成的元组:
- 对 java.lang.invoke.MethodHandle 类型实例的引用,
- 对于 java.lang.invoke.MethodType 类的一个实例的引用,
- 对于 Class、java.lang.invoke.MethodHandle、java.lang.invoke.MethodType 和 String 类的实例的引用。 在调用位置说明符中对方法句柄的符号引用进行解析期间, 或者在调用位置说明符中对方法描述符的符号引用(即对任何静态参数的符号引用)进行解析期间, 或者在对任何静态参数的符号引用进行解析期间,与方法类型或方法句柄解析相关的任何异常都可能被抛出。
访问控制
如果且仅当以下任一条件成立时,类或接口 C 才能被类或接口 D 访问:
- C 是公共类。
- C 和 D 属于同一个运行时包(§5.3)。
如果且仅当以下任何一种情况成立时,字段或方法 R 才能被类或接口 D 访问:
- R 是公共的。
- R 是受保护的,并且在类 C 中声明,而 D 或者是 C 的子类或者 C 本身。此外,如果 R 不是静态的,那么对 R 的符号引用必须包含对类 T 的符号引用,其中 T 是 D 的子类、D 的超类或者 D 本身。
- R 是受保护的或者具有默认访问权限(即既不是公共也不是受保护也不是私有的),并且由与 D 属于同一运行时包的类声明。
- R 是私有的并且在 D 中声明。 关于访问控制的这一讨论忽略了对受保护字段访问或方法调用的目标的有关限制(目标必须是类 D 或 D 的子类)。该要求作为验证过程的一部分进行检查;它不是链接时访问控制的一部分
覆盖
在类 C 中声明的实例方法 mC 会覆盖类 A 中声明的另一个实例方法 mA,当且仅当以下条件之一成立时:
- mC 与 mA 完全相同;
- 类 C 是类 A 的子类;
- mC 的名称和描述符与 mA 相同;
- mC 不被标记为 ACC_PRIVATE;
- 以下任一条件成立:
- mA 被标记为 ACC_PUBLIC;或者被标记为 ACC_PROTECTED;或者既不是 ACC_PUBLIC 也不是 ACC_PROTECTED 也不是 ACC_PRIVATE,且类 A 与类 C 属于相同的运行时包;
- mC 覆盖了方法 m'(m' 与 mC 和 mA 都不同),并且 m' 覆盖了 mA。
初始化
类或接口的初始化过程包括执行其类或接口的初始化方法。 类或接口 C 只能在以下情况下被初始化:
- 执行任何一条引用 C 的 Java 虚拟机指令(new、getstatic、putstatic 或 invokestatic)(§new、§getstatic、§putstatic、§invokestatic)。这些指令直接或通过字段引用或方法引用间接地引用一个类或接口。 在执行一条 new 指令时,如果该引用的类尚未初始化,则该类会被初始化。 在执行一条 getstatic、putstatic 或 invokestatic 指令时,如果已声明的已解析字段或方法对应的类尚未初始化,则该类或接口会被初始化。
- 对于方法处理程序的第 2 类(REF_getStatic)、第 4 类(REF_putStatic)、第 6 类(REF_invokeStatic)或第 8 类(REF_newInvokeSpecial)方法处理程序的首次调用(§5.4.3.5),该方法处理程序实例是通过方法处理程序解析(§5.4.3.5)得到的。 这意味着当启动方法时,启动方法的类会被初始化。当遇到调用动态方法指令(§invokedynamic)时,会调用此方法,这是对调用点说明符进行持续解析的一部分。
- 在类库中某些反射方法的调用,例如在类 Class 或包 java.lang.reflect 中。
- 如果 C 是一个类,则其子类的初始化。
- 如果 C 是一个声明了非抽象、非静态方法的接口,则直接或间接实现该接口的类的初始化。
- 如果 C 是一个类,则在 Java 虚拟机启动时将其指定为初始类(§5.2)。 在初始化之前,类或接口必须进行链接,即进行验证、准备,并且可以进行选择性的解析。 由于 Java 虚拟机是多线程的,因此类或接口的初始化需要仔细的同步处理,因为其他线程可能在同一时间尝试初始化同一个类或接口。还存在这样的可能性,即类或接口的初始化可能作为该类或接口初始化的一部分被递归地请求。Java 虚拟机的实现负责处理同步和递归操作。通过以下步骤进行初始化。它假定类对象已经经过验证和准备,并且该类对象包含表明四种情况之一的状态信息:
- 此类对象已通过验证和准备,但尚未初始化。
- 此类对象正由某个特定线程进行初始化。
- 此类对象已完全初始化并可使用。
- 此类对象处于错误状态,可能是由于尝试初始化但失败所致。 对于每个类或接口 C,都有一个唯一的初始化锁 LC。从 C 到 LC 的映射由 Java 虚拟机实现自行决定。 例如,LC 可以是 C 的类对象,或者与该类对象相关联的监视器。初始化 C 的步骤如下:
- 对 C 语言的初始化锁 LC 进行同步操作。这需要让当前线程等待直至能够获取到该锁。
- 如果 C 类的对象表明由其他线程正在进行 C 的初始化过程,那么释放 LC 并使当前线程暂停,直到收到初始化过程已完成的通知为止。之后再重复此操作。 初始化过程的执行不会影响线程的中断状态。程序。
- 如果 C 类的对象表明当前线程正在对 C 进行初始化操作,那么这就必定是关于初始化的递归请求。发布 LC 正常且功能完备。
- 如果 C 类的对象表明 C 已经被初始化完毕,那么就无需再进行任何操作。释放 LC 并正常完成即可。
- 如果 C 类的类对象处于错误状态,那么初始化就无法进行。释放 LC 并抛出 NoClassDefFoundError 异常。 加载、链接和初始化 初始化 5.5359
- 否则,记录当前线程正在为类 C 初始化对象这一事实,并释放 LC。 然后,按照每个最终静态字段在 ClassFile 结构中出现的顺序,用其 ConstantValue 属性中的常量值来初始化 C 类的每个最终静态字段。
- 接下来,如果 C 是一个类而非接口,并且其超类尚未被初始化,那么就将 C 的超类设为 SC,将 C 的所有超接口(无论是直接还是间接的)设为 SI1、...、SIn(这些超接口中至少有一个是非抽象且非静态的方法)。超接口的顺序是通过对 C 所直接实现的每个接口的超接口层次结构进行递归枚举来确定的。对于 C 所直接实现的每个接口 I(按照 C 的接口数组的顺序),枚举会递归地在 I 的超接口(按照 I 的接口数组的顺序)中进行()
- 接下来,通过查询 C 的定义类加载器来确定其是否启用了断言功能。
- 接下来,执行 C 类或接口的初始化方法。
- 如果类或接口的初始化方法的执行过程顺利完成,那么获取 LC(锁),将 C 类对象的标签设为已完全初始化状态,通知所有等待的线程,释放 LC,然后正常完成此过程。
- 否则,类或接口的初始化方法必须因抛出某个异常 E 而突然终止。如果 E 类的类型不是 Error 或其子类,则创建一个 ExceptionInInitializerError 类的新实例,并将 E 作为参数传递给该实例,然后在接下来的步骤中用这个对象代替 E。如果由于发生 OutOfMemoryError 而无法创建 ExceptionInInitializerError 的新实例,则在接下来的步骤中使用一个 OutOfMemoryError 对象来代替 E。
- 获取 LC,将类对象 C 的标签设为错误状态,通知所有等待的线程,释放 LC,并在步骤 6 中突然完成此过程,原因采用 E 或在前一步中确定的替代值。 Java 虚拟机实现可以优化此过程,即在能够确定类的初始化已经完成时,省略步骤 1 中的锁获取(以及步骤 4/5 中的释放),前提是根据 Java 内存模型,在执行此优化时仍然存在如果获取了锁则会存在的所有“已发生之前”顺序(JLS 第 17.4.5 节)。
绑定原生方法实现
绑定是将用非 Java 编程语言编写且实现了原生方法的函数集成到 Java 虚拟机中以便其能够执行的过程。尽管这一过程传统上被称为链接,但在规范中使用“绑定”这一术语是为了避免与 Java 虚拟机对类或接口的链接操作产生混淆。
Java 虚拟机退出
当某个线程调用了 Runtime 类或 System 类的 exit 方法,或者调用了 Runtime 类的 halt 方法,并且安全管理者允许这种退出操作时,Java 虚拟机就会退出。
此外,JNI(Java 本地接口)规范描述了在使用 JNI 调用 API 加载和卸载 Java 虚拟机时 Java 虚拟机的终止过程。