在Inside the Java Virtual Machine - Chapter 7 The Lifetime of a Type - Initialization中,有这么一段:
A use of a non-constant static field is an active use of only the class or interface that actually declares the field. For example, a field declared in a class may be referred to via a subclass. A field declared in an interface may be referred to via a subinterface or class that implements the interface. These are passive uses of the subclass, subinterface, or class that implements the interface--uses that won't trigger their initialization. They are an active use only of the class or interface in which the field is actually declared. Here's an example that illustrates this principle:
// On CD-ROM in file classlife/ex2/NewParent.java class NewParent { static int hoursOfSleep = (int) (Math.random() * 3.0); static { System.out.println("NewParent was initialized."); } } // On CD-ROM in file classlife/ex2/NewbornBaby.java class NewbornBaby extends NewParent { static int hoursOfCrying = 6 + (int) (Math.random() * 2.0); static { System.out.println("NewbornBaby was initialized."); } } // On CD-ROM in file classlife/ex2/Example2.java 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."); } }
然后它说:
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.
我明白“NewbornBaby is not initialized”,但是为什么“NewbornBaby need not be loaded”呢?如果 NewbornBaby 都没有被 loaded,那么 JVM 怎么可能知道 hoursOfSleep 是来源于 NewParent 的呢?
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
虽然结果显示 NewbornBaby 被 loaded 了,但是为什么 NewParent 先于 NewbornBaby 被 loaded 呢?
一个相关的问题:inheritance - How JVM loads parent classes in Java - Stack Overflow。Kalhan.Toress的comment表达了和我一样的疑惑。既然子类没有先被loaded,JVM是怎么知道子类的父类是什么呢?我不太明白Keppil的回复所说的内容。
正文java -verbose:class Example2
后面那段是我自己没有认真看清楚的问题,日志显示的是loaded,只是说NewParent先被loaded完成,之后NewbornBaby才被loaded完成,其实NewbornBaby是先被loaded的,并不是NewParent 先于 NewbornBaby 被 loaded。
我的问题更多的是为什么“NewbornBaby need not be loaded”。关于这个问题,我在Stack Overflow上面提了一个问题jvm - Why does "Inside the Java Virtual Machine" say "NewbornBaby need not be loaded"? - Stack Overflow,Holger的回答说“NewbornBaby need not be loaded”是错误的,我也同意ta所说的。
1
ik2h 2020-09-05 11:44:04 +08:00
就算从不创建类的实例,类字段也和类关联,类字段会在调用构造方法前初始化,javac 会为每个类自动生成一个类初始化方法,类字段会在这个方法里初始化,类的初始化是内部方法,对程序员不可见.
如果我没记错,可以用 javap 看看初始化方法, 你给的这个例子全是 static,直接看深入 static 方面的知识就好了 |
2
JasonLaw OP @ik2h #1 你说的都是关于 initialization 的,而我的问题是关于 loading 的。
顺便说一下,“javac 会为每个类自动生成一个类初始化方法”不是完全正确的。 关于什么情况会产生()方法,https://www.artima.com/insidejvm/ed2/lifetype4.html 中描述了很清楚,以下是一些片段: Not all classes will necessarily have a () method in their class file. If a class declares no class variables or static initializers, it won't have a () method. If a class declares class variables, but doesn't explicitly initialize them with class variable initializers or static initializers, it won't have a () method. If a class contains only class variable initializers for static final variables, and those class variable initializers use compile-time constant expressions, that class won't have a () method. Only those classes that actually require Java code to be executed to initialize class variables to proper initial values will have a class initialization method. Interfaces may also be awarded a () method in the class file. All fields declared in an interface are implicitly public, static, and final and must be initialized with a field initializer. If an interface has any field initializers that don't resolve at compile-time to a constant, that interface will have a () method. |
3
ik2h 2020-09-05 12:40:33 +08:00
@JasonLaw https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html
A reference to a static field (§8.3.1.1) causes initialization of only the class or interface that actually declares it, even though it might be referred to through the name of a subclass, a subinterface, or a class that implements an interface. |
4
Jooooooooo 2020-09-05 12:52:22 +08:00
load 子类肯定要先 load 父类吧
|
5
JasonLaw OP @ik2h #3 先抛开其他的,单纯讨论 3 楼所引用的内容。
“A reference to a static field (§8.3.1.1) causes initialization of only the class or interface that actually declares it”不一定正确。主题正文中的外链(因为提示回复不能包含外链,所以只能这么弄)说了“A use of a field that is both static and final, and initialized by a compile-time constant expression, is not an active use of the type that declares the field.”,其中的 Example3 也演示了(虽然 Example3 中引用了 Angry.greeting 和 Dog.greeting,但是 Angry 和 Dog 都没有被初始化)。 还是我哪里理解错了? |
7
ik2h 2020-09-05 13:42:22 +08:00
@JasonLaw 这方面是关于类的主动引用和被动引用, 我记得 final static 在 java 编程思想里面也有提及这方面的内容
|
8
JasonLaw OP @ik2h #6 是的,所以“A reference to a static field (§8.3.1.1) causes initialization of only the class or interface that actually declares it”不一定正确呀。难道“a field that is both static and final, and initialized by a compile-time constant expression”不是一个“static field”?
|
9
JasonLaw OP @Jooooooooo #4 你可以看看第一条附言。既然子类没有先被 loaded,JVM 是怎么知道子类的父类是什么呢?
|
10
ik2h 2020-09-05 14:05:29 +08:00
@JasonLaw 《深入理解 Java 虚拟机》 7.2 有比较详细的解释,编译时常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
|
11
Jooooooooo 2020-09-05 14:09:22 +08:00
@JasonLaw 可以打开 .class 文件看一下 main 方法的字节码是怎么样的. 特别是这一句 int hours = NewbornBaby.hoursOfSleep; 引用的常量池是怎么对应的
|
12
JasonLaw OP @Jooooooooo #11 "int hours = NewbornBaby.hoursOfSleep;"对应的字节码为"getstatic #2 和 istore_1"。在 constant pool 中,index 为 2 的 entry 是一个 CONSTANT_Fieldref_info,通过 CONSTANT_Fieldref_info 中的 class_index 最后会得到 NewbornBaby,通过 CONSTANT_Fieldref_info 中的 name_and_type_index,最后会得到一个 CONSTANT_NameAndType_info,通过 CONSTANT_NameAndType_info 中的 name_index 最后会得到 hoursOfSleep,通过 CONSTANT_NameAndType_info 中的 descriptor_index 最后会得到 I 。
然后呢?为什么父类先于子类被 loaded ?子类没有先被 loaded,JVM 是怎么知道子类的父类是什么呢? |
14
mind3x 2020-09-07 04:49:13 +08:00
@JasonLaw
> “A reference to a static field (§8.3.1.1) causes initialization of only the class or interface that actually declares it”不一定正确。 @ik2h 在 #3 中引用的是 JLS,虽然版本旧了点,好歹也是正宗官方的语言 specification,何来的“不一定正确”?你关心的问题本来就应该从 JLS 和 JVM Spec 里面寻找答案。 至于你的核心问题 NewParent 为什么先于 NewbornBaby 被 load,原因很简单:类加载是一边解析一边递归的——这里的顺序是 开始加载 NewbornBaby -> 解析 NewbornBaby -> 发现父类 NewParent -> (递归) 开始加载 NewParent -> ... -> 加载 NewParent 结束 -> 继续加载 NewbornBaby -> 加载 NewParent 结束。你看到的 log 只是打了 load 结束而已。 JVM Spec (1.7 版) 5.3: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.3 > A subtlety here is that recursive class loading to load superclasses is performed as part of resolution (§5.3.5, step 3). Therefore, a ClassNotFoundException that results from a class loader failing to load a superclass must be wrapped in a NoClassDefFoundError. |
15
mind3x 2020-09-07 04:50:40 +08:00
|
16
JasonLaw OP @Jooooooooo #4 子类还是要先被 loaded 的,只是父类先被 loaded 完成,具体可以看看第 2 条附言。
|
17
JasonLaw OP |
18
JasonLaw OP @mind3x #14 你说“A reference to a static field (§8.3.1.1) causes initialization of only the class or interface that actually declares it”来源于官方文档,一定是正确的。难道“a field that is both static and final, and initialized by a compile-time constant expression”不是一个“static field”?
比如我将 NewbornBaby 改为下面这样: ``` class NewbornBaby extends NewParent { static int hoursOfCrying = 6 + (int) (Math.random() * 2.0); static final int hoursOfSleep = 6; // a field that is both static and final, and initialized by a compile-time constant expression static { System.out.println("NewbornBaby was initialized."); } } ``` 运行 java -verbose Example2,可以看出 NewbornBaby 被没有被 loaded 。 |
19
Jooooooooo 2020-09-07 10:35:53 +08:00
@JasonLaw 你最早看的那篇是文章是错的蛮坑的.
|
20
JasonLaw OP @Jooooooooo #19 其实 Inside the Java Virtual Machine 还是很好的,现在看了 Chapter 5-7,只发现了这一个错误,其他的都解释得很好。
|
21
mind3x 2020-09-07 15:10:14 +08:00
@JasonLaw
#17 "NewbornBaby need not be loaded" 这句话是错的,很多书都有错,这个正常。以 JLS, JVMS 为准。 #18 没错,"A reference to a static field (§8.3.1.1) causes initialization of only the class or interface that actually declares it" 是正确的。这里的重点是 "reference" (敲黑板)。你加了 final,这个值就是个常量,直接在编译时放进 Example2 的常量池了,而不是引用 NewbornBaby 。你可以看字节码的区别。 |