JVM递归class初始化实现

JVM recursive class initialization implementation

正在研究 JVM spec/internals,并想了解循环引用递归 class 初始化应该如何正确进行。看这个例子:

 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. If the Class object for C indicates that initialization is in progress for C by the current thread, then this must be a recursive request for initialization. Release LC and complete normally.

这意味着在我上面的第 3 步中,JVM 耸耸肩,然后返回完成第 2 步。但是完成第 2 步意味着在新的 CB 实例上调用构造函数 <init>。当 class CB 尚未完成其 <clinit> 时,它如何做到这一点?

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

这只有效,因为这些是 static 字段 (other),如果您删除该修饰符 - 您将得到一个 Whosebug(因为例如字段,初始化移至构造函数)。在我看来,如果我向您展示编译器实际在做什么,事情可能会变得显而易见?

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


}

编辑

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

 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.