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 初始化时,必须 运行 像这样:
- CB
<clinit>
实例化一个 CA,应该触发...
- CA
<clinit>
,实例化一个CB,它尝试
- 再次CB
<clinit>
,已经在进行中...
JVM 规范在 s5.5 初始化下说:
- 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();
,因此,必须调用构造函数并因此初始化 ivar
。 ivar
设置为 IVAR
,但后者将仅在 构造函数完成后 初始化。所以当试图设置 ivar
时,它会拆箱 IVAR
的值,此时(因为 CB
没有完全初始化)是 null
.
正在研究 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 初始化时,必须 运行 像这样:
- CB
<clinit>
实例化一个 CA,应该触发... - CA
<clinit>
,实例化一个CB,它尝试 - 再次CB
<clinit>
,已经在进行中...
JVM 规范在 s5.5 初始化下说:
- 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();
,因此,必须调用构造函数并因此初始化 ivar
。 ivar
设置为 IVAR
,但后者将仅在 构造函数完成后 初始化。所以当试图设置 ivar
时,它会拆箱 IVAR
的值,此时(因为 CB
没有完全初始化)是 null
.