为什么 "Inside the Java Virtual Machine" 说 "NewbornBaby need not be loaded"?

Why does "Inside the Java Virtual Machine" say "NewbornBaby need not be loaded"?

Inside the Java Virtual Machine - Chapter 7 The Lifetime of a Type - Initialization 有如下代码片段。

class NewParent {

    static int hoursOfSleep = (int) (Math.random() * 3.0);

    static {
        System.out.println("NewParent was initialized.");
    }
}

class NewbornBaby extends NewParent {

    static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);

    static {
        System.out.println("NewbornBaby was initialized.");
    }
}

class Example2 {

    // Invoking main() is an active use of Example2
    public static void main(String[] args) {

        // Using hoursOfSleep is an active use of NewParent,
        // but a passive use of NewbornBaby
        int hours = NewbornBaby.hoursOfSleep;
        System.out.println(hours);
    }

    static {
        System.out.println("Example2 was initialized.");
    }
}

然后说在上面的例子中,执行Example2的main()只会导致Example2和NewParent被初始化。 NewbornBaby没有初始化,不需要加载.

Example2引用了NewbornBaby,我觉得应该是“JVM一开始加载NewbornBaby然后发现NewbornBaby没有hoursOfSleep字段,然后它继续加载 NewbornBaby 的超类 NewParent”。那么,为什么在Java虚拟机里面不需要加载NewbornBaby

经过javac Example2.java,我运行java -verbose:class Example2,下面是部分输出。

[Loaded Example2 from file:/Users/jason/trivial/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
Example2 was initialized.
[Loaded NewParent from file:/Users/jason/trivial/]
[Loaded NewbornBaby from file:/Users/jason/trivial/]
[Loaded java.lang.Math$RandomNumberGeneratorHolder from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.Random from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
NewParent was initialized.
1

证明JVM确实加载了NewbornBaby

在这种情况下,正如您在书中引用的那样,JVM 首先加载 NewbornBaby,它发现 NewbornBaby 没有 hoursOfSleep 字段,然后它继续加载 NewbornBaby 的 superclass NewParent":JVM 尝试加载 NewbornBaby 但此 class 是 NewParent 的子 class所以要加载它需要加载它的 superclass 的所有 class 方法,所以要加载 newbornBaby(first) 需要加载 NewParent(second) 作为正在加载 newbornBaby.

在恢复 NewParent 的加载时,看起来它首先加载,但它确实加载了,但请记住这是加载 newbornBaby(已经启动的进程)[=22= 的子进程]

您 运行 陷入了 class 加载 初始化 .

的常见混淆

您链接的文章描述了初始化,这是由某些 well defined actions:

触发的

§12.4.1. When Initialization Occurs

A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

  • T is a class and an instance of T is created.
  • A static method declared by T is invoked.
  • A static field declared by T is assigned.
  • A static field declared by T is used and the field is not a constant variable (§4.12.4).

您的代码正在访问 class NewParent 中的 static 字段,这将触发 class 的初始化。您访问它的方式无关紧要。所以当你 运行 你的代码没有记录时,它会打印

Example2 was initialized.
NewParent was initialized.
1

因此 NewbornBaby 尚未初始化,因为执行了 none 个指定的触发操作。

Class loading 然而,是完全不同的事情。它的时间是有意未指定的,除了它必须发生在初始化之前。 JVM 可能急切地加载所有引用的 classes,甚至在应用程序启动之前,或者延迟加载,直到验证器或应用程序需要它。


此时,重要的是要了解编译器将检查引用的 static 字段是否存在并在 class NewParent 中找到它,它会生成字节码仍然使用源代码中使用的类型。所以,在运行时间加载指定的classNewbornBaby是不可避免的(文章在这方面是错误的),即使它不会被初始化(文章似乎与加载中)。

JLS, §13.1. The Form of a Binary比较:

Given a legal expression denoting a field access in a class C, referencing a field named f that is not a constant variable and is declared in a (possibly distinct) class or interface D, we define the qualifying type of the field reference as follows:

...

  • If the reference is of the form TypeName.f, where TypeName denotes a class or interface, then the class or interface denoted by TypeName is the qualifying type of the reference.

...

The reference to f must be compiled into a symbolic reference to the erasure (§4.6) of the qualifying type of the reference, plus the simple name of the field, f.

换句话说,表达式 NewbornBaby.hoursOfSleep 将使用 NewbornBaby 作为限定类型进行编译,并且 运行 时间必须在超类型中再次找到实际字段,例如编译器做到了。如果在 运行 时 NewbornBaby 的不同版本具有该名称和类型的匹配字段,则使用该字段。

无法在 运行 时间加载 class NewbornBaby,以找出适用的场景。


此外,off-specification 将记录 class 加载。看来,它不会在触发加载时发生,而是在加载完成时发生。这确实已经包括了一些验证步骤,包括加载和检查 superclass 是否存在且兼容(即不是 interface、不是 final 等)。

因此,当验证者遇到对 class NewbornBaby 的访问时,它会触发 class 的加载,从而触发 NewParent 的加载。但是 NewParent 的加载首先完成并首先报告,因为它的完成对于完成随后记录的 NewbornBaby 的加载是必要的。

但是,如前所述,这是特定于实现的。只有初始化被精确指定。