【问题标题】:JVM recursive class initialization implementationJVM递归类初始化实现
【发布时间】:2020-10-21 19:23:03
【问题描述】:

正在研究 JVM 规范/内部结构,并想了解循环引用的递归类初始化应该如何正确发生。看这个例子:

 class CA extends Object {
    public final int ivar = 1;
    public static CB other = new CB(); 
    public CA() {
        System.out.println("in CA.init, my ivar is " + this.ivar); 
    }   
}
class CB extends Object {
    public final int ivar = 2;
    public static CA other = new CA();  
    public CB() {
        System.out.println("in CB.init, my ivar is " + this.ivar); 
    }
    
    public static void main(String[] args) {
        CB cb = new CB();  
    }
}

执行此结果:

in CB.init, my svar is 2
in CA.init, my ivar is 1
in CB.init, my svar is 2

这些反映了实例初始化并且有意义。 class 初始化,但必须像这样运行:

  1. CB <clinit> 实例化一个 CA,它应该触发...
  2. CA <clinit>,它实例化一个 CB,它尝试一个
  3. CB <clinit> 再次进行中...

JVM 规范在 s5.5 初始化下说:

  1. 如果 C 的 Class 对象指示当前线程正在对 C 进行初始化,则这必须是初始化的递归请求。释放 LC 并正常完成。

这意味着在我上面的第 3 步中,JVM 耸了耸肩,然后返回完成第 2 步。但是完成第 2 步意味着在新的 CB 实例上调用构造函数 <init>。当 CB 类还没有完成它的<clinit> 时,它怎么能做到这一点?

在这种情况下,因为对象没有对它们持有的彼此实例“做任何事情”,所以没有伤害就没有犯规。但是我应该如何考虑这里的行为和潜在的陷阱?谢谢。

【问题讨论】:

    标签: java jvm language-lawyer


    【解决方案1】:

    这仅起作用,因为那些是 静态 字段 (other),如果您删除该修饰符 - 您将得到一个 StackOverflow(因为对于实例字段,初始化被移动到构造函数)。在我看来,如果我向您展示编译器实际上在做什么,事情可能会变得很明显?

    static class CA extends Object {
    
        public final int ivar = 1;
        public static CB other;
    
        static {
            System.out.println("running CA static block");
            other = new CB();
            System.out.println("CB done");
        }
    
        public CA() {
            System.out.println("in CA.init, my ivar is " + ivar);
        }
    }
    
    static class CB extends Object {
    
        public final int ivar = 2;
        public static CA other;
    
        static {
            System.out.println("running CB static block");
            other = new CA();
            System.out.println("CA done");
        }
    
        public CB() {
            System.out.println("in CB.init, my ivar is " + ivar);
        }
    
    
    }
    

    编辑

    在类完全初始化之前弄乱调用哪些实例方法确实很危险。你可能会踩到你意想不到的东西:

     static class CB {
    
        private static final CB ONLY = new CB();
    
        private static final Integer IVAR = 42;
        public final int ivar = IVAR;
    
    }
    
    public static void main(String[] args) {
        System.out.println(CB.ONLY.ivar);
    }
    

    这会引发NullPointerException。为什么?您可以自己反编译并查看,但用相当简化的话:

    • ivar 在构造函数中通过读取IVAR 变量进行初始化

    • 静态是按照它们在代码中出现的顺序执行的

    因此,首先执行private static final CB ONLY = new CB();,因此必须调用构造函数并初始化ivarivar 设置为IVAR,但后者将仅在构造函数完成后 初始化。所以当尝试设置ivar时,它会拆箱IVAR的值,此时(因为CB没有完全初始化)是null

    【讨论】:

    • 谢谢。这很好地说明了此示例的工作原理——实际上in CB.init... 是在CB done 之前打印的,这证实了CB 的<clinit> 在成功创建CB 实例时尚未完全完成。同样,在这种情况下,这不会导致语义问题,但仍然意味着实例方法(init)可以在类“完全初始化”之前运行。公平地说,当它起作用并且存在 语义冲突时,它最终会在运行时对你造成打击,这是公平的吗? (堆栈溢出还是其他?)
    • @BenZotto 我不完全确定你的意思,但the difference between clinit and init 可能有帮助吗?
    • 不,我明白了那里的区别——我的问题是,当我运行您的示例版本时,CB 的构造函数(由“in CB.init ...”表示)被执行 CB 的类初始化完成之前(由“CB 完成”指示)。这意味着在某些情况下,在类本身完全“初始化”之前对类的实例运行实例方法是“可以的”。这对我来说是一个令人惊讶的结果,我想了解这是否只是在它起作用时起作用(这种情况),如果不起作用就会爆炸。
    • @BenZotto 对。但是编译器不知道那里有一个null,所以它只是用Integer::valueOf编译它...
    • 这对于enum 类型来说真的很烦人。由于语法不允许在常量之前使用静态初始化器,因此所有静态字段(编译时常量除外)通常在实例构造函数运行后初始化。
    猜你喜欢
    • 1970-01-01
    • 2017-05-10
    • 2020-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-21
    • 2018-02-21
    • 2019-08-06
    相关资源
    最近更新 更多