JVM recursive class initialization implementation

Viewed 95

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:

  1. CB <clinit> instantiates a CA, which should trigger...
  2. CA <clinit>, which instantiates a CB, which attempts a
  3. CB <clinit> again, which is already in-progress...

The JVM spec says under s5.5 Initialization:

  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.

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 Answers

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 the IVAR 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.

Related