The answer comes from JLS section 12.4. We start with 12.4.1:
12.4.1. When Initialization Occurs
A class or interface type T will be initialized immediately before the first occurrence of any one of the following:
T is a class and an instance of T is created.
A static method declared by T is invoked.
A static field declared by T is assigned.
A static field declared by T is used and the field is not a constant variable (§4.12.4).
When A.main
is invoked, the third clause applies, and thus class A (and by extension A.test
) must be initialized. The initialization of A.test
triggers the fourth clause above, meaning that B must now be initialized. B must now access fields of A, but A has not yet initialized.
We need to now dig into section 12.4.2:
12.4.2. Detailed Initialization Procedure
...
For each class or interface C, there is a unique initialization lock LC. The mapping from C to LC is left to the discretion of the Java Virtual Machine implementation. The procedure for initializing C is then as follows:
Synchronize on the initialization lock, LC, for C. This involves waiting until the current thread can acquire LC.
If the Class object for C indicates that initialization is in progress for C by some other thread, then release LC and block the current thread until informed that the in-progress initialization has completed, at which time repeat this step.
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.
Step 3 is the one we take here. Class A is already undergoing initialization by the current thread, so we are in a recursive initialization situation. We thus just give up and complete normally right away, so A.test remains null and B.test gets the value of A.test which is null.