2

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.

Ben Zotto
  • 70,108
  • 23
  • 141
  • 204

1 Answers1

2

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.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 1
    Thanks. This is a good clarifier of how this example works--and indeed `in CB.init...` is printed before `CB done` which confirms that CB's `` has not fully completed at a time when a instance of CB is successfully created. And again, in this case, that causes no semantic problems, but nonetheless means that instance methods (init) can be run before a class is "fully initialized". Is it fair to say that this works when it works and when there *is* a semantic conflict, it will just end up blowing up on you at runtime? (With a stack overflow or otherwise?) – Ben Zotto Oct 21 '20 at 20:29
  • @BenZotto I am not entirely sure what you mean, but does [the difference between clinit and init](https://stackoverflow.com/questions/8517121/java-what-is-the-difference-between-init-and-clinit) helps may be? – Eugene Oct 21 '20 at 20:33
  • 1
    No, I get the distinction there-- my question here is that when I run your version of the example, the constructor for CB (indicated by "in CB.init...") gets executed *before* the completion of the class initialization for CB (indicated by "CB done"). That means that it's "OK" in some circumstances to run instance methods on instances of a class before the class itself has been fully "initialized". That was a surprising consequence for me, and I want to understand if that just sort of works when it works (this case) and blows up if it doesn't. – Ben Zotto Oct 21 '20 at 20:37
  • @BenZotto I came up with an example where it breaks, hopefully this will help. – Eugene Oct 22 '20 at 10:55
  • Excellent, thanks for coming up with this. It sounds like the lesson here is that there's no magic-- if the recursive initialization happens to be semantically harmless it will "work", but if you do anything that logic suggests you can't, it will just break as here. I note too that `NullPointerException` is a sort of tell for this kind of failure (since an `int` field does not take a `null` in general). – Ben Zotto Oct 22 '20 at 15:11
  • 1
    @BenZotto right. but the compiler does not know that there is a `null` coming there, so it simply compiles that with `Integer::valueOf`... – Eugene Oct 22 '20 at 15:14
  • 3
    This can be really annoying with `enum` types. Since the syntax does not allow static initializers before the constants, all static fields (except compile-time constants) are initialized after the instance constructors ran, in general. – Holger Oct 22 '20 at 16:00