英文:
JVM recursive class initialization implementation
问题
正在研究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 ivar is 2
in CA.init, my ivar is 1
in CB.init, my ivar is 2
这些反映了实例的初始化,是合理的。然而,类的初始化必须像这样运行:
- CB
<clinit>
实例化一个 CA,应该触发... - CA
<clinit>
,实例化一个 CB,然后尝试... - 再次运行 CB
<clinit>
,但此时已经在进行中...
JVM规范在第5.5节“初始化”中说:
> 3. 如果类C的Class对象表明当前线程正在为C进行初始化,则这必须是对初始化的递归请求。释放LC并正常完成。
这意味着在上述的步骤3中,JVM会耸耸肩,然后返回完成步骤2。但是完成步骤2意味着调用新CB实例上的构造函数<init>
。当类CB尚未完成其<clinit>
时,它怎么能做到呢?
在这种情况下,因为这些对象并未“对它们持有的每个实例做任何操作”,所以没有什么坏处。但我应该如何思考这种行为以及潜在的风险?谢谢。
英文:
Am studying JVM spec/internals, and would like to understand how circularly-referenced recursive class initialization is supposed to happen correctly. Looking at this example:
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();
}
}
Executing this results in:
in CB.init, my svar is 2
in CA.init, my ivar is 1
in CB.init, my svar is 2
Those reflect the instance initializations and make sense. The class inits though, must run like this:
- CB
<clinit>
instantiates a CA, which should trigger... - CA
<clinit>
, which instantiates a CB, which attempts a - CB
<clinit>
again, which is already in-progress...
The JVM spec says under s5.5 Initialization:
> 3. 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.
This implies that at my step 3 above, the JVM shrugs, and goes back to finish step 2. But completing step 2 means calling the constructor <init>
on a new CB instance. How can it do that when class CB has not completed its <clinit>
?
In this case, because the objects are not "doing anything" with the instances of each other they hold, no harm no foul. But how should I be thinking about the behavior and the potential pitfalls here? Thanks.
答案1
得分: 2
这仅在这些是 静态 字段(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();
,因此必须调用构造函数,从而初始化 ivar
。ivar
被设置为 IVAR
,但后者只会在构造函数完成后才初始化。因此,在尝试设置 ivar
时,它将对 IVAR
的值进行拆箱,而此时(因为 CB
尚未完全初始化)它是 null
。
英文:
This only works because those are static fields (other
), if you remove that modifier - you will get a StackOverflow
(because for instance fields, the initialization is moved to the constructor). It seems to me that if I show you what the compiler is actually doing, things might get obvious?
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);
}
}
EDIT
Messing with what instance methods are called, until the class is fully initialized is indeed dangerous. You might be stepping on things you would not expect:
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);
}
This throws a NullPointerException
. Why? You can decompile yourself and see, but in rather simplified words:
-
ivar
is initialized in the constructor by reading theIVAR
variable -
statics are executed in the order of how they appear in code
So, first private static final CB ONLY = new CB();
is executed, as such, constructor must be called and thus ivar
initialized. ivar
is set to IVAR
, but the latter will be initializes only after the constructor finishes. So when trying to set ivar
, it will unbox the value of IVAR
, which at this point (because CB
is not fully initialized) is null
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论