“Inside the Java Virtual Machine”为什么说”NewbornBaby不需要被加载”?

huangapple go评论81阅读模式
英文:

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 {

    // 调用 main() 是对 Example2 的主动使用
    public static void main(String[] args) {

        // 使用 hoursOfSleep 是对 NewParent 的主动使用,
        // 但对 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

英文:

Inside the Java Virtual Machine - Chapter 7 The Lifetime of a Type - Initialization has code snippet like below.

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.");
    }
}

And then it says In the above example, executing main() of Example2 causes only Example2 and NewParent to be initialized. NewbornBaby is not initialized and need not be loaded.

Example2 references NewbornBaby, I think it should be "JVM loads NewbornBaby at first and it finds that NewbornBaby doesn't have hoursOfSleep field, then it proceeds to load NewbornBaby's superclass NewParent". So, why does Inside the Java Virtual Machine say NewbornBaby need not be loaded?

After javac Example2.java, I run java -verbose:class Example2, below is part of the output.

[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

It demonstrates that the JVM does indeed load NewbornBaby.

答案1

得分: 4

你遇到了类的“加载”和“初始化”常见的混淆。

你所链接的文章描述的是“初始化”,它是由一些明确定义的动作触发的:

§12.4.1. 初始化发生的时机

类型 T(类或接口)将会在以下任何一个动作首次发生之前被初始化:

  • T 是一个类,并且创建了 T 的一个实例。
  • T 声明的一个静态方法被调用。
  • T 声明的一个静态字段被赋值。
  • T 声明的一个静态字段被使用,并且该字段不是一个常量变量(§4.12.4)。

你的代码正在访问 NewParent 类中的一个静态字段,这将触发该类的初始化。你访问它的方式是不相关的。因此,当你在没有日志记录的情况下运行代码时,它会打印:

Example2 被初始化。
NewParent 被初始化。
1

因此,NewbornBaby 并没有被初始化,因为没有执行指定的触发动作。

然而,类的“加载”是完全不同的事情。它的时机故意未指定,除了必须在初始化之前发生。JVM 可能会在应用程序启动之前急切地加载所有引用的类,也可能会推迟加载,直到验证器或应用程序需要为止。


在这一点上,重要的是要理解,虽然编译器会检查所引用的 static 字段是否存在,并且会在 NewParent 类中找到它,但它仍然会产生字节码,仍然使用在源代码中使用的类型。因此,在运行时加载指定的 NewbornBaby 类是不可避免的(在这方面,文章是错误的),即使它不会被初始化(这是该文章似乎与加载混淆的地方)。

JLS,§13.1. 二进制的形式 进行比较:

鉴于用于表示类 C 中字段访问的合法表达式,引用了一个名为 f 的字段,该字段不是一个常量变量,并且在(可能不同的)类或接口 D 中声明,我们定义字段引用的修饰类型如下:

...

  • 如果引用采用 TypeName.f 的形式,其中 TypeName 表示类或接口,则由 TypeName 表示的类或接口是引用的修饰类型。

...

f 的引用必须被编译为对引用的修饰类型的擦除(§4.6)的符号引用,再加上字段的简单名称 f

换句话说,表达式 NewbornBaby.hoursOfSleep 将使用 NewbornBaby 作为修饰类型进行编译,运行时必须再次在父类型中找到实际的字段,就像编译器所做的那样。如果在运行时存在一个不同版本的 NewbornBaby,它有一个匹配的同名同类型字段,那么该字段将被使用。

无论如何,在运行时都无法避免加载 NewbornBaby 类,以确定适用于哪种情况。


此外,类加载何时被记录是不符合规范的。似乎加载被触发时并不会发生记录,而是在加载完成时记录。这已经包括了一些验证步骤,包括加载并检查超类是否存在且兼容(即不是 interface,不是 final 等)。

因此,当验证器遇到对类 NewbornBaby 的访问时,它触发加载该类,从而触发加载 NewParent 类。但是 NewParent 的加载会首先完成并首先被报告,因为必须完成 NewParent 的加载才能完成 NewbornBaby 的加载,后者稍后被记录。

但是,正如前面所说,这是特定于实现的。只有初始化是精确规定的。

英文:

You ran into a common confusion of class loading and initialization.

The article you've linked, describes initialization, which is triggered by some 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).

Your code is accessing a static field in class NewParent which will trigger the initialization of that class. The way you access it, is irrelevant. So when you run your code without logging, it printed

Example2 was initialized.
NewParent was initialized.
1

So NewbornBaby has not been initialized, as none of the specified trigger actions were performed.

Class loading, however, is an entirely different thing. Its timing is intentionally unspecified, except that it must happen before initialization. A JVM may eagerly load all referenced classes, even before the application starts, or defer the loading, until either, the verifier or the application needs it.


At this point, it's important to understand that while the compiler will check whether the referenced static field exists and will find it in the class NewParent, it will produce bytecode still using the type that has been used in the source code. So, loading the specified class NewbornBaby at runtime is unavoidable (the article is wrong in this regard), even if it won't get initialized (which the article seems to confuse with loading).

Compare with 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.

In other words, the expression NewbornBaby.hoursOfSleep will get compiled using NewbornBaby as the qualifying type and the runtime has to find the actual field again in the supertype, like the compiler did. If there was a different version of NewbornBaby at runtime having a matching field of that name and type, that field was used instead.

There is no way around loading the class NewbornBaby at runtime, to find out which scenario applies.


Further, it's off-specification when the class loading will be logged. It seems, it does not happen when the loading is triggered, but when the loading completed. This does already include some verification steps, including loading and checking whether the superclass exists and is compatible (i.e. not an interface, not final, etc.).

So when the verifier encounters an access to class NewbornBaby, it triggers the loading of that class, which triggers the loading of NewParent. But the loading of NewParent completes first and is reported first, as its completion is necessary to complete the loading of NewbornBaby which is logged afterwards.

But, as said, that's implementation specific. Only the initialization is precisely specified.

答案2

得分: 1

在这种情况下,正如您在书中引用的那样,JVM 首先加载了 NewbornBaby 类,然后发现 NewbornBaby 没有 hoursOfSleep 字段,因此它继续加载了 NewbornBaby 的父类 NewParent:"JVM 尝试加载 NewbornBaby,但这个类是 NewParent 的子类,因此在加载 newbornBaby 时需要加载其父类的所有类方法,所以要加载 newbornBaby(第一个),需要加载 NewParent(第二个)作为加载 newbornBaby 的一部分。

总之,加载 NewParent 看起来是首先加载的,确实如此,但请记住,这是加载 newbornBaby 的子过程(已经开始的过程)。

英文:

In this case as you quote in the book JVM loads NewbornBaby at first and it finds that NewbornBaby doesn't have hoursOfSleep field, then it proceeds to load NewbornBaby's superclass NewParent": the JVM try to load the NewbornBaby but this class is a subclass of NewParent so to be load it needs to load all the class methods of its superclass, so to load the newbornBaby(first) is necessary to load NewParent(second) as part of loading newbornBaby.

In resume the load of NewParent, looks like it loads first, and it does but remember that this is a subprocess of loading the newbornBaby(process that already started)

huangapple
  • 本文由 发表于 2020年9月6日 00:16:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/63756025.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定