Java 类加载的解决阶段实际上从哪里开始?

2022-09-01 19:33:06

我刚刚读完了Java虚拟机规范,关于类加载的部分让我感到困惑。就我一般的理解而言,在阅读了规范之后,我认为类的整体实例化由以下步骤组成,顺序如下:

  • 创建/加载:类装入器定位表示类的字节流,可以是文件或网络流,也可以是实现类装入器要获取的任何内容。如果找不到任何类,则抛出 a。此时,已经发生了一些基本的验证,如果字节数组不表示 Java 类(例如,缺少幻数),或者如果正在运行的 JVM 实例不支持类版本,则抛出 a。ClassNotFoundExceptionClassFormatErrorUnsupportedClassVersionError

  • 链接:类被挂接到 JVM 中。如果出现问题,则会抛出 一个子类。链接由三个子步骤组成:LinkageError

    • 验证:确保字节流表示 Java 类,例如,没有正式错误的字节码,例如方法字节码的溢出操作数堆栈。如果类验证失败,则抛出 。VerifyError

    • 准备:JVM 为所有静态字段分配内存,并可能创建一个实例模板来加快实例创建速度。创建虚拟方法表。在此阶段不会引发特定于类加载的错误。(不过可能会抛出一个。OutOfMemoryError

    • 解决方法:现在以运行时常量池的形式加载到方法区域中的所有符号引用都将解析为此 JVM 加载的实际类型。如果符号引用可以解析,但导致定义冲突,则抛出 a。如果找不到引用的类,则抛出 a,它基本上包装了一个由类装入器尝试装入此被引用类所抛出的类。如果引用的类引用自身,则抛出 a。解决方案可以以两种类型之一进行,这取决于JVM的实现者IncompatibleClassChangeErrorNoClassDefFoundErrorClassNotFoundExceptionClassCircularityError

      1. Eager:现在已解析对其他字段、方法或类的所有符号引用。

      2. 惰性:符号引用的解析被推迟到第一次使用方法时。这可能会带来引用不存在的类永远不会引发错误,如果此引用永远不需要解析。

  • 初始化:运行在类中定义为 Java 代码的类初始值设定项。如果异常是由此类初始值设定项引起的,则此异常将重新包装在 .staticExceptionInInitializerError

令我困惑的是上述类加载机制的解决阶段。为什么分辨率被定义为链接中的显式步骤,该步骤在准备后专门发生?在加载阶段的描述中,已经提到

如果 C 具有任何直接超接口,则使用 §5.4.3.1 的算法解析从 C 到其直接超接口的符号引用。

进行验证时,符号引用是否也未得到解决,因为验证被描述

验证 (§4.10) 确保类或接口的二进制表示形式在结构上是正确的 (§4.9)。验证可能会导致加载其他类和接口 (§5.3),但不需要验证或准备它们。

我总是想着这张照片

Java class loading overview

资料来源:http://www.programcreek.com

我几乎在任何地方都看到过解释类加载。是否应该将解决方案视为所有阶段,创建/加载验证链接初始化(因为解决方案可以懒惰地完成)的所有阶段的一部分。

目前,我认为将分辨率阶段从此图像中移除并声明为可以随时使用的一般过程是有意义的,因为在任何阶段都可能需要有关其他类的信息,因此需要加载此类类,这必然还需要对该类的符号引用的解析。从所示图片来看,分辨率似乎只发生在一连串单独事件中的特定点。

我怀疑,这种对决议是一个专门步骤的描述可能只是一个时代的遗产,在那个时代,决议从未懒惰地进行过,而是在所有剩余的象征性引用都得到解决的地方。

我想知道的是:今天的JVM中的分辨率是否应该像我描述的那样被理解?还是我错了,分辨率仍然可以被理解为固定时间线中的专用步骤,就像图像显示的那样?


答案 1

您的图片显示,在准备后始终显示解析,但这不起作用。需要直接超类进行准备,因为您需要了解超类的实例字段,以确定特定类的对象实例内存布局。此外,类的静态初始值设定项及其超类必须在可以使用类之前执行,即在创建实例或调用静态方法之前。

这与所有其他引用类型的解析不同,后者可以延迟更长的时间。允许在首次调用方法之前解析方法中使用的类型。

当你看第5.4.3章的开头。解决方案,有明确说明:

Java 虚拟机指令 anewarraycheckcastgetfieldgetstaticinstanceofinvokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualldcldc_w, multianewarraynewputfield, and putstatic 对运行时常量池进行符号引用。执行任何这些指令都需要解析其符号引用。

因此,差异非常明显。直接超类和直接实现的接口(或接口情况下的超接口)的解析发生得更早,并且出于上述字节码指令的目的,符号引用的解析可以推迟。


答案 2

很难说,但我认为你只是在文档中发现了一点差异或歧义。恕我直言,文档中的步骤没有非常精确地定义,因此实现可能有点具体,步骤实际上可能重叠一点,等等。执行工作的主要关切可能是速度,而不是绝对的逻辑清晰度。

尝试查看OpenJDK的源代码,您可能会发现一些有趣的东西。


推荐