JVM规范系列第5章:加载、链接与初始化

Posted by 陈树义 on 2021-05-22

加载是根据特定名称查找类或接口类型的二进制表示(Binary Representation),并由此二进制表示创建类或接口的过程。

加载,就是指去寻找类或接口的过程。

链接是为了让类或接口可以被 Java 虚拟机执行,而将类或接口并入虚拟机运行时状态的过程。

链接,就是将类或接口与JVM链接起来的过程。

类或接口的初始化是指执行类或接口的初始化方法(§ 2.9)

初始化,就是执行方法的过程。

Java 虚拟机为每个类型都维护一个常量池。

这里注意关键词「每个类型」,也就是说整型是一个常量池,字符串类型也是一个常量池。

运行时常量池中的所有引用最初都是符号引用。

符号引用的意思是它只是一个符号,需要后续通过链接,替换为具体的内存地址。

这里说的符号引用,下面列举几种:

  • CONSTANT_Fieldref_info 类或接口的某个字段的符号引用
  • CONSTANT_Methodref_info 类中某个方法的符号引用
  • CONSTANT_InterfaceMethodref_info 接口的某个方法的符号引用
  • CONSTANT_MethodHandle_info 方法句柄的符号引用
  • 等等

创建和加载

Java 虚拟机的启动是通过引导类加载器(Bootstrap Class Loader § 5.3.1) 创建一个初始类(Initial Class)来完成,这个类是由虚拟机的具体实现指定。紧接着, Java 虚拟机链接这个初始类,初始化并调用它的 public void main(String[])方法。之后的整个执行过程都是由对此方法的调用开始。执行 main 方法中的 Java 虚拟机指令可能会导致 Java 虚拟机链接另外的一些类或接口,也可能会调用另外的方法。

简单地说,虚拟机通过链接初始类,由此会调用其他类或接口,从而开始整个庞大Java项目的运行。

首先,Java 虚拟机检查引导类加载器是否是已加载过的标记为 N 的类或接口的初始加载器。如果是的话,这个类或接口就是 C,并且不再创建其它类型。否则, Java 虚拟机将参数 N 传递给引导类加载器的特定方法,以平台相关的方式搜索 C 的描述。典型的情况是,类或文件会被表示为树型文件系统中的某个文件,类或接口的名称就是此文件的路径名。

这段话描述了引导类加载器如何加载类或接口,可以详细看看。

首先, Java 虚拟机检查 L 是否为已经加载过的标识为 N 的类或接口的初始加载器。如果是的话,那个类或接口就是 C,不用再创建其它类了。否则 Java 虚拟机会调用 L 的 loadClass(N)①方法。 这次调用的返回值就是创建好的类或接口 C。 Java 虚拟机会记录下 L 是 C 的初始加载器(§ 5.3.4)。这节其余的部分会更详细地描述这个过程。

这段话描述了自定义类加载器如何加载类或接口。

链接

类加载器需要特别考虑到类型的安全链接问题。一种可能出现的情况是,当两个不同的类加载器初始加载标记为 N 的类或接口时,在每个加载器里 N 表示着不同的类或接口。

这里的意思或许是说,一个同样的类被加载在不同的类加载器中,其表示两个完全不同的类。即使这些类或接口的字节码完全相同。

《Java 虚拟机规范》 允许灵活地选择链接(并且会有递归加载)发生的时机。

链接过程可以灵活选择。

例如,Java 虚拟机实现可以选择只有在使用类或接口中符号引用时才去逐一解析它(延迟解析),或是当类在验证时就解析每个引用(预先解析)。这意味着在一些虚拟机实现中,在类或接口被初始化动作开始后,解析动作可能还正在进行。

解析过程可以延迟解析,也可以预先解析。

验证(Verification, § 4.10)阶段用于确保类或接口的二进制表示结构上是正确的。验证过程可能会导致某些额外的类和接口被加载进来(§ 5.3),但不应该会导致它们也需要验证或准备。

验证会导致其他类的加载但不会导致它们也需要验证或准备。

准备(Preparation)阶段的任务是为类或接口的静态字段分配空间,并用默认值初始化这些字段(§ 2.3, § 2.4)。这个阶段不会执行任何的虚拟机字节码指令。

准备阶段是为类或接口的静态字段分配空间,并用默认值初始化这些字段。注意,并不会执行任何虚拟机字节码指令。

Java 虚拟机指令 anewarray、 checkcast、 getfield、 getstatic、 instanceof、nvokedynamic、 invokeinterface、 invokespecial、 invokestatic、 invokevirtual、ldc、 ldc_w、 multianewarray、 new、 putfield 和 putstatic将符号引用指向运行时常量池。执行上述任何一条指令都需要对它的符号引用的进行解析。

解析就是解析符号引用的过程,将其转为具体的值。

解析(Resolution)是根据运行时常量池的符号引用来动态决定具体的值的过程。

接下来的大部分内容,都是对于特定内容的解析步骤,例如:对于类或接口解析、字段解析、普通方法解析、接口方法解析、调用点限定符解析等。这部分内容确实晦涩难懂,建议大致通读一遍就好,暂时不要深究。

初始化

初始化(Initialization) 对于类或接口来说,就是执行它的初始化方法(§ 2.9)。

这里就是开始执行初始化方法了,包括两个初始化方法。具体有5种情况下会触发这种初始化。

1、在执行下列需要引用类或接口的 Java 虚拟机指令时: new, getstatic, putstatic或 invokestatic。这些指令通过字段或方法引用来直接或间接地引用其它类。执行上面所述的 new 指令,在类或接口没有被初始化过时就初始化它。执行上面的 getstatic,
putstatic 或 invokestatic 指令时,那些解析好的字段或方法中的类或接口如果还没有被初始化那就初始化它。
2、在初次调用 java.lang.invoke.MethodHandle 实例时,它的执行结果为通过 Java虚拟机解析出类型是 2(REF_getStatic)、 4(REF_putStatic)或者 6(REF_invokeStatic)的方法句柄(§ 5.4.3.5)。
3、在调用 JDK 核心类库中的反射方法时,例如, Class 类或 java.lang.reflect 包。
4、在对于类的某个子类的初始化时。
5、在它被选定为 Java 虚拟机启动时的初始类(§ 5.2) 时。