V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
JasonLaw
V2EX  ›  Java

关于“Inside the Java Virtual Machine - Chapter 7 The Lifetime of a Type - Initialization”的疑问

  •  1
     
  •   JasonLaw · 2020-09-05 11:22:03 +08:00 · 1838 次点击
    这是一个创建于 1336 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 呢?

    第 1 条附言  ·  2020-09-05 13:41:57 +08:00

    一个相关的问题:inheritance - How JVM loads parent classes in Java - Stack OverflowKalhan.Toress的comment表达了和我一样的疑惑。既然子类没有先被loaded,JVM是怎么知道子类的父类是什么呢?我不太明白Keppil的回复所说的内容

    第 2 条附言  ·  2020-09-07 09:42:37 +08:00

    正文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 OverflowHolger的回答说“NewbornBaby need not be loaded”是错误的,我也同意ta所说的。

    22 条回复    2020-09-07 18:56:19 +08:00
    ik2h
        1
    ik2h  
       2020-09-05 11:44:04 +08:00
    就算从不创建类的实例,类字段也和类关联,类字段会在调用构造方法前初始化,javac 会为每个类自动生成一个类初始化方法,类字段会在这个方法里初始化,类的初始化是内部方法,对程序员不可见.

    如果我没记错,可以用 javap 看看初始化方法, 你给的这个例子全是 static,直接看深入 static 方面的知识就好了
    JasonLaw
        2
    JasonLaw  
    OP
       2020-09-05 12:25:35 +08:00
    @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.
    ik2h
        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.
    Jooooooooo
        4
    Jooooooooo  
       2020-09-05 12:52:22 +08:00
    load 子类肯定要先 load 父类吧
    JasonLaw
        5
    JasonLaw  
    OP
       2020-09-05 13:04:08 +08:00
    @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 都没有被初始化)。

    还是我哪里理解错了?
    ik2h
        6
    ik2h  
       2020-09-05 13:35:09 +08:00
    @JasonLaw 编译时常量
    ik2h
        7
    ik2h  
       2020-09-05 13:42:22 +08:00
    @JasonLaw 这方面是关于类的主动引用和被动引用, 我记得 final static 在 java 编程思想里面也有提及这方面的内容
    JasonLaw
        8
    JasonLaw  
    OP
       2020-09-05 13:43:52 +08:00
    @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”?
    JasonLaw
        9
    JasonLaw  
    OP
       2020-09-05 13:44:58 +08:00
    @Jooooooooo #4 你可以看看第一条附言。既然子类没有先被 loaded,JVM 是怎么知道子类的父类是什么呢?
    ik2h
        10
    ik2h  
       2020-09-05 14:05:29 +08:00
    @JasonLaw 《深入理解 Java 虚拟机》 7.2 有比较详细的解释,编译时常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
    Jooooooooo
        11
    Jooooooooo  
       2020-09-05 14:09:22 +08:00
    @JasonLaw 可以打开 .class 文件看一下 main 方法的字节码是怎么样的. 特别是这一句 int hours = NewbornBaby.hoursOfSleep; 引用的常量池是怎么对应的
    JasonLaw
        12
    JasonLaw  
    OP
       2020-09-05 15:11:28 +08:00
    @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 是怎么知道子类的父类是什么呢?
    JasonLaw
        13
    JasonLaw  
    OP
       2020-09-05 15:14:55 +08:00
    @ik2h #10 我感觉,我们根本不是在讨论一个东西,相关的回复也没有什么上下文关系。
    mind3x
        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.
    mind3x
        15
    mind3x  
       2020-09-07 04:50:40 +08:00
    @mind3x #14

    typo: "继续加载 NewbornBaby -> 加载 NewParent 结束" 应为 "继续加载 NewbornBaby -> 加载 NewbornBaby" 结束。
    JasonLaw
        16
    JasonLaw  
    OP
       2020-09-07 09:48:05 +08:00
    @Jooooooooo #4 子类还是要先被 loaded 的,只是父类先被 loaded 完成,具体可以看看第 2 条附言。
    JasonLaw
        17
    JasonLaw  
    OP
       2020-09-07 09:51:07 +08:00
    @mind3x #14
    @mind3x #15

    “为什么 NewParent 先于 NewbornBaby 被 loaded”是我自己没有认真看清楚的问题,我的核心问题并不是这个,我的核心问题是“为什么 NewbornBaby need not be loaded”。关于这个问题,Holger 在 Stack Overflow 上面回答我了,具体可以看看第 2 条附言。
    JasonLaw
        18
    JasonLaw  
    OP
       2020-09-07 09:58:49 +08:00
    @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 。
    Jooooooooo
        19
    Jooooooooo  
       2020-09-07 10:35:53 +08:00
    @JasonLaw 你最早看的那篇是文章是错的蛮坑的.
    JasonLaw
        20
    JasonLaw  
    OP
       2020-09-07 10:44:30 +08:00
    @Jooooooooo #19 其实 Inside the Java Virtual Machine 还是很好的,现在看了 Chapter 5-7,只发现了这一个错误,其他的都解释得很好。
    mind3x
        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 。你可以看字节码的区别。
    JasonLaw
        22
    JasonLaw  
    OP
       2020-09-07 18:56:19 +08:00 via iPhone
    @mind3x 如果对“a reference to a static field”是这样理解的话,那句话的确是对的。我的关注点是 source code,而你的是 compiled code 。总之谢谢你的回复。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1158 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 17:55 · PVG 01:55 · LAX 10:55 · JFK 13:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.