Search code examples
javajvmclassloader

Why does "Inside the Java Virtual Machine" say "NewbornBaby need not be loaded"?


Inside the Java Virtual Machine - Chapter 7 The Lifetime of a Type - Initialization has code snippet like below.

class NewParent {

    static int hoursOfSleep = (int) (Math.random() * 3.0);

    static {
        System.out.println("NewParent was initialized.");
    }
}

class NewbornBaby extends NewParent {

    static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);

    static {
        System.out.println("NewbornBaby was initialized.");
    }
}

class Example2 {

    // Invoking main() is an active use of Example2
    public static void main(String[] args) {

        // Using hoursOfSleep is an active use of NewParent,
        // but a passive use of NewbornBaby
        int hours = NewbornBaby.hoursOfSleep;
        System.out.println(hours);
    }

    static {
        System.out.println("Example2 was initialized.");
    }
}

And then it says In the above example, executing main() of Example2 causes only Example2 and NewParent to be initialized. NewbornBaby is not initialized and need not be loaded.

Example2 references NewbornBaby, I think it should be "JVM loads NewbornBaby at first and it finds that NewbornBaby doesn't have hoursOfSleep field, then it proceeds to load NewbornBaby's superclass NewParent". So, why does Inside the Java Virtual Machine say NewbornBaby need not be loaded?

After javac Example2.java, I run java -verbose:class Example2, below is part of the output.

[Loaded Example2 from file:/Users/jason/trivial/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
Example2 was initialized.
[Loaded NewParent from file:/Users/jason/trivial/]
[Loaded NewbornBaby from file:/Users/jason/trivial/]
[Loaded java.lang.Math$RandomNumberGeneratorHolder from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.Random from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
NewParent was initialized.
1

It demonstrates that the JVM does indeed load NewbornBaby.


Solution

  • You ran into a common confusion of class loading and initialization.

    The article you've linked, describes initialization, which is triggered by some well defined actions:

    §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).

    Your code is accessing a static field in class NewParent which will trigger the initialization of that class. The way you access it, is irrelevant. So when you run your code without logging, it printed

    Example2 was initialized.
    NewParent was initialized.
    1
    

    So NewbornBaby has not been initialized, as none of the specified trigger actions were performed.

    Class loading, however, is an entirely different thing. Its timing is intentionally unspecified, except that it must happen before initialization. A JVM may eagerly load all referenced classes, even before the application starts, or defer the loading, until either, the verifier or the application needs it.


    At this point, it's important to understand that while the compiler will check whether the referenced static field exists and will find it in the class NewParent, it will produce bytecode still using the type that has been used in the source code. So, loading the specified class NewbornBaby at runtime is unavoidable (the article is wrong in this regard), even if it won't get initialized (which the article seems to confuse with loading).

    Compare with JLS, §13.1. The Form of a Binary:

    Given a legal expression denoting a field access in a class C, referencing a field named f that is not a constant variable and is declared in a (possibly distinct) class or interface D, we define the qualifying type of the field reference as follows:

    ...

    • If the reference is of the form TypeName.f, where TypeName denotes a class or interface, then the class or interface denoted by TypeName is the qualifying type of the reference.

    ...

    The reference to f must be compiled into a symbolic reference to the erasure (§4.6) of the qualifying type of the reference, plus the simple name of the field, f.

    In other words, the expression NewbornBaby.hoursOfSleep will get compiled using NewbornBaby as the qualifying type and the runtime has to find the actual field again in the supertype, like the compiler did. If there was a different version of NewbornBaby at runtime having a matching field of that name and type, that field was used instead.

    There is no way around loading the class NewbornBaby at runtime, to find out which scenario applies.


    Further, it's off-specification when the class loading will be logged. It seems, it does not happen when the loading is triggered, but when the loading completed. This does already include some verification steps, including loading and checking whether the superclass exists and is compatible (i.e. not an interface, not final, etc.).

    So when the verifier encounters an access to class NewbornBaby, it triggers the loading of that class, which triggers the loading of NewParent. But the loading of NewParent completes first and is reported first, as its completion is necessary to complete the loading of NewbornBaby which is logged afterwards.

    But, as said, that's implementation specific. Only the initialization is precisely specified.