Skip to content

Laboratory · Class loading

How does the JVM find, load, verify and initialize your classes?

Three stations, from the whole subsystem down to one class: the loaders of a running JVM, a request travelling between them, and the life of a class from its bytes to its first use. Every result can be checked against real JDK 25 output.

Station 1 of 3 · Loader observatory

Who loads the classes of a running Java program?

Make a prediction

Make a prediction

A running Java program uses java.lang.String, java.sql.Connection and your own class Delegation. How many different class loaders defined them?

Advanced

Investigations

Questions that come up in real systems, each answered with output from a real JVM: identity and namespaces, initialization order and its failures, unloading and leaks, and what each loading error means.

Identity and namespaces

  • Defining versus initiating loadersWhich loaders “have” a class after a request?

    Two loaders end up knowing a class: the one that built it, and the one that was asked for it. The loaders in between only passed the question along and forget it.

    jcmd <pid> VM.classloaders show-classes (while the plugin is loaded)Output
    +-- <bootstrap>
          |
          |               Classes: java.lang.Object
          |                        [Ljava.lang.Object;
          …
          |                        (825 classes)
          |
          |        Hidden Classes: java.lang.invoke.LambdaForm$MH/0x0000000043041000
          |                        (1 hidden class)
          |
          +-- "platform", jdk.internal.loader.ClassLoaders$PlatformClassLoader
                |
                +-- "app", jdk.internal.loader.ClassLoaders$AppClassLoader
                      |
                      |               Classes: Unload
                      |                        (1 class)
                      |
                      +-- "plugin", java.net.URLClassLoader
    
                                            Classes: com.example.plugin.Hello
                                                     (1 class)
    

    Verified with OpenJDK 25.0.4 · Linux x64; 823 of the bootstrap loader's class names are left out

  • ClassCastException between two classes with one nameHow can a cast from Message to Message fail?

    The object was made from the plugin’s Message; the variable expects the application’s Message. They share a name, not an identity.

    java -cp app DelegationOutput
    parent-first.loadClass("com.example.shared.Message") -> defined by app
    parent-first.loadClass("com.example.plugin.Greeter") -> defined by parent-first
    child-first.loadClass("com.example.shared.Message") -> defined by child-first
    child-first.loadClass("java.lang.String") -> defined by the bootstrap loader
    java.lang.ClassNotFoundException: com.example.Missing
    same runtime class: false
    java.lang.ClassCastException: class com.example.shared.Message cannot be cast to class com.example.shared.Message (com.example.shared.Message is in unnamed module of loader 'child-first' @764c12b6; com.example.shared.Message is in unnamed module of loader 'app')
    java.lang.LinkageError: loader constraint violation: when resolving method 'void com.example.shared.Bus.send(com.example.shared.Message)' the class loader 'child-first' @764c12b6 of the current class, com/example/plugin/Greeter, and the class loader 'app' for the method's defining class, com/example/shared/Bus, have different Class objects for the type com/example/shared/Message used in the signature (com.example.plugin.Greeter is in unnamed module of loader 'child-first' @764c12b6, parent loader 'app'; com.example.shared.Bus is in unnamed module of loader 'app')
    

    Verified with OpenJDK 25.0.4 · Linux x64 (hash codes vary between runs)

  • Loading constraintsWhen does the JVM stop two loaders from disagreeing?

    When code from one loader calls a method of a class from another loader, both must agree on every class named in that method. Otherwise the call is refused.

    plugin/com/example/plugin/Greeter.javaJava
    package com.example.plugin;
    
    import com.example.shared.Bus;
    import com.example.shared.Message;
    
    public class Greeter {
        public static void run() {
            Bus.send(new Message("hello from the plugin"));
        }
    }

    Verified with OpenJDK 25.0.4 · Linux x64

    java -cp app DelegationOutput
    parent-first.loadClass("com.example.shared.Message") -> defined by app
    parent-first.loadClass("com.example.plugin.Greeter") -> defined by parent-first
    child-first.loadClass("com.example.shared.Message") -> defined by child-first
    child-first.loadClass("java.lang.String") -> defined by the bootstrap loader
    java.lang.ClassNotFoundException: com.example.Missing
    same runtime class: false
    java.lang.ClassCastException: class com.example.shared.Message cannot be cast to class com.example.shared.Message (com.example.shared.Message is in unnamed module of loader 'child-first' @764c12b6; com.example.shared.Message is in unnamed module of loader 'app')
    java.lang.LinkageError: loader constraint violation: when resolving method 'void com.example.shared.Bus.send(com.example.shared.Message)' the class loader 'child-first' @764c12b6 of the current class, com/example/plugin/Greeter, and the class loader 'app' for the method's defining class, com/example/shared/Bus, have different Class objects for the type com/example/shared/Message used in the signature (com.example.plugin.Greeter is in unnamed module of loader 'child-first' @764c12b6, parent loader 'app'; com.example.shared.Bus is in unnamed module of loader 'app')
    

    Verified with OpenJDK 25.0.4 · Linux x64 (hash codes vary between runs)

  • Modules and layersHow do modules decide which loader defines a class?

    Since Java 9 the platform is split into modules, and each module is given to one loader. A loader that is asked for a class in a module sends the request to that module’s loader.

    java -cp . LoadersOutput
    java.lang.String | module java.base | defined by the bootstrap loader (null)
    java.util.logging.Logger | module java.logging | defined by the bootstrap loader (null)
    java.sql.Connection | module java.sql | defined by platform
    Loaders | unnamed module @c387f44 | defined by app
    com.sun.tools.javac.Main | module jdk.compiler | defined by app
    app loader: app, parent: platform
    platform loader: platform, parent: null
    platform.loadClass("com.sun.tools.javac.Main"): defined by app, same Class object: true
    

    Verified with OpenJDK 25.0.4 · Linux x64 (hash codes vary between runs)

Initialization

  • Circular initializationWhat does a class see when initialization goes round in a circle?

    A needs B and B needs A. The JVM does not loop forever: when B asks for A, which is already being set up, it gets A’s variables as they are at that moment, still at their defaults.

    Circular.javaJava
    public class Circular {
        public static void main(String[] args) {
            System.out.println("A.a = " + A.a + ", B.b = " + B.b);
        }
    }
    
    class A {
        static int a = B.b + 1;
    }
    
    class B {
        static int b = A.a + 1;
    }

    Verified with OpenJDK 25.0.4 · Linux x64

    java -cp . CircularOutput
    A.a = 2, B.b = 1
    

    Verified with OpenJDK 25.0.4 · Linux x64

  • Initialization errorsWhat happens when a static initializer throws?

    The first time, the program sees the real problem. After that, the class is marked broken for good, and every later use fails immediately.

    InitFailure.javaJava
    public class InitFailure {
        public static void main(String[] args) {
            for (int attempt = 1; attempt <= 2; attempt++) {
                try {
                    System.out.println(Fragile.value);
                } catch (Throwable t) {
                    System.out.println("attempt " + attempt + ": " + t);
                    System.out.println("  cause: " + t.getCause());
                }
            }
        }
    }
    
    class Fragile {
        static int value = 1 / zero();
    
        static int zero() {
            return 0;
        }
    }

    Verified with OpenJDK 25.0.4 · Linux x64

    java -cp . InitFailureOutput
    attempt 1: java.lang.ExceptionInInitializerError
      cause: java.lang.ArithmeticException: / by zero
    attempt 2: java.lang.NoClassDefFoundError: Could not initialize class Fragile
      cause: java.lang.ExceptionInInitializerError: Exception java.lang.ArithmeticException: / by zero [in thread "main"]
    

    Verified with OpenJDK 25.0.4 · Linux x64

  • Initialization locks and deadlockCan two threads deadlock just by using two classes?

    Each class can be set up by only one thread at a time. If thread one sets up Left and needs Right, while thread two sets up Right and needs Left, both wait forever.

    InitDeadlock.java (the two classes)Java
    // Thread "first" reads Left.value, thread "second" reads Right.value, at the same time.
    class Left {
        static int value = pause() + Right.value;
    
        static int pause() {
            try { Thread.sleep(200); } catch (InterruptedException e) { }
            return 1;
        }
    }
    
    class Right {
        static int value = Left.pause() + Left.value;
    }

    Verified with OpenJDK 25.0.4 · Linux x64

    jcmd <pid> Thread.print (the two threads, 4 s later)Output
    "first" #23 [2323] prio=5 os_prio=0 cpu=0.91ms elapsed=4.17s tid=0x00007efd6c1603a0 nid=2323 in Object.wait()  [0x00007efd44cf6000]
       java.lang.Thread.State: RUNNABLE
    	at Left.<clinit>(InitDeadlock.java:15)
    	- waiting on the Class initialization monitor for Right
    	at InitDeadlock.lambda$main$0(InitDeadlock.java:3)
    	…
    
    "second" #24 [2324] prio=5 os_prio=0 cpu=0.53ms elapsed=4.17s tid=0x00007efd6c161640 nid=2324 in Object.wait()  [0x00007efd44bf6000]
       java.lang.Thread.State: RUNNABLE
    	at Right.<clinit>(InitDeadlock.java:24)
    	- waiting on the Class initialization monitor for Left
    	at InitDeadlock.lambda$main$1(InitDeadlock.java:4)
    	…
    

    Verified with OpenJDK 25.0.4 · Linux x64; thread ids and addresses vary

Unloading and leaks

  • Class unloadingWhen does a class leave memory?

    Never on its own. A class can go only together with the loader that built it, and only once nothing can reach that loader, its classes or their objects.

    Unload.javaJava
    URL[] plugin = { Path.of("plugin").toUri().toURL() };
    URLClassLoader loader = new URLClassLoader("plugin", plugin, ClassLoader.getSystemClassLoader());
    Class<?> hello = loader.loadClass("com.example.plugin.Hello");
    System.out.println(hello.getMethod("greet").invoke(null));
    loader.close();
    loader = null;
    hello = null;
    System.out.println("dropped every reference to the loader; requesting a collection");
    System.gc();
    System.out.println("done");

    Verified with OpenJDK 25.0.4 · Linux x64

    java -Xlog:class+load,class+unload -cp . Unload (the plugin lines)Output
    [0.022s][info][class,load] com.example.plugin.Hello source: file:/…/plugin/
    hello from a plugin
    dropped every reference to the loader; requesting a collection
    [0.029s][info][class,unload] unloading class com.example.plugin.Hello 0x000000001f040800
    done
    

    Verified with OpenJDK 25.0.4 · Linux x64 (G1, the default collector here; System.gc() is a request the JVM may ignore)

    jcmd <pid> VM.classloader_statsOutput
    ClassLoader         Parent              CLD*               Classes   ChunkSz   BlockSz  Type
    0x0000000043001810  0x00000000421c95c8  0x00007fbdc415eaf0       1      6144      1304  java.net.URLClassLoader
    0x00000000421c95c8  0x00000000421c9ae8  0x00007fbdc4150660       1      6144      2848  jdk.internal.loader.ClassLoaders$AppClassLoader
    0x00000000421c9ae8  0x0000000000000000  0x00007fbdc40d63b0       0         0         0  jdk.internal.loader.ClassLoaders$PlatformClassLoader
    0x0000000000000000  0x0000000000000000  0x00007fbdc40c6a10     825   4456448    227256  <boot class loader>
                                                                     1      3072      1736   + hidden classes
    Total = 4                                                      828   4471808    233144
    ChunkSz: Total size of all allocated metaspace chunks
    BlockSz: Total size of all allocated metaspace blocks (each chunk has several blocks)
    

    Verified with OpenJDK 25.0.4 · Linux x64; addresses differ between runs

  • Class loader leaksWhy does Metaspace grow after every redeploy?

    One forgotten reference to anything from an old plugin keeps the whole plugin in memory: its loader, every class it loaded, and their data.

    jcmd <pid> VM.classloader_statsOutput
    ClassLoader         Parent              CLD*               Classes   ChunkSz   BlockSz  Type
    0x0000000043001810  0x00000000421c95c8  0x00007fbdc415eaf0       1      6144      1304  java.net.URLClassLoader
    0x00000000421c95c8  0x00000000421c9ae8  0x00007fbdc4150660       1      6144      2848  jdk.internal.loader.ClassLoaders$AppClassLoader
    0x00000000421c9ae8  0x0000000000000000  0x00007fbdc40d63b0       0         0         0  jdk.internal.loader.ClassLoaders$PlatformClassLoader
    0x0000000000000000  0x0000000000000000  0x00007fbdc40c6a10     825   4456448    227256  <boot class loader>
                                                                     1      3072      1736   + hidden classes
    Total = 4                                                      828   4471808    233144
    ChunkSz: Total size of all allocated metaspace chunks
    BlockSz: Total size of all allocated metaspace blocks (each chunk has several blocks)
    

    Verified with OpenJDK 25.0.4 · Linux x64; addresses differ between runs

Errors and diagnostics

  • Six errors, and where each one comes fromClassNotFoundException or NoClassDefFoundError? VerifyError or ClassFormatError?

    Each error names a different stage. The class cannot be found, it was there when compiling but is missing now, its bytes are damaged, it was made for a newer Java, its code is unsafe, or two loaders disagree.

    Class.forName("com.example.Missing")Output
    Exception in thread "main" java.lang.ClassNotFoundException: com.example.Missing
    	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
    	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
    	at java.base/java.lang.Class.forName0(Native Method)
    	at java.base/java.lang.Class.forName(Class.java:467)
    	at java.base/java.lang.Class.forName(Class.java:458)
    	at FindMissing.main(FindMissing.java:3)
    

    Verified with OpenJDK 25.0.4 · Linux x64

    Uses.main calls Helper.help(); Helper.class was deleted after compilingOutput
    main started
    Exception in thread "main" java.lang.NoClassDefFoundError: Helper
    	at Uses.main(Uses.java:4)
    Caused by: java.lang.ClassNotFoundException: Helper
    	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
    	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
    	... 1 more
    

    Verified with OpenJDK 25.0.4 · Linux x64

    defineClass with damaged bytes: a wrong magic number, then a truncated fileOutput
    Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691583 in class file Future
    	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    	…
    
    Exception in thread "main" java.lang.ClassFormatError: Truncated class file
    	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    	…
    

    Verified with OpenJDK 25.0.4 · Linux x64; byte 3 changed from 0xBE to 0xBF (3405691583 = 0xCAFEBABF), and the file cut to 40 bytes

    A class file whose major version was raised from 69 to 70Output
    $ od -A d -t x1 -N 8 Future.class
    0000000 ca fe ba be 00 00 00 45
    $ od -A d -t x1 -N 8 future/Future.class
    0000000 ca fe ba be 00 00 00 46
    
    $ java -cp future Future
    Error: LinkageError occurred while loading main class Future
    	java.lang.UnsupportedClassVersionError: Future has been compiled by a more recent version of the Java Runtime (class file version 70.0), this version of the Java Runtime only recognizes class file versions up to 69.0
    

    Verified with OpenJDK 25.0.4 · Linux x64

    Loading Broken.class: make() returns an int where an Object is declaredOutput
    $ javap -c Broken.class
    public class Broken {
      public static java.lang.Object make();
        Code:
             0: iconst_1
             1: areturn
    }
    
    $ java -cp . LoadBroken
    Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
    Exception Details:
      Location:
        Broken.make()Ljava/lang/Object; @1: areturn
      Reason:
        Type integer (current frame, stack[0]) is not assignable to reference type
      Current Frame:
        bci: @1
        flags: { }
        locals: { }
        stack: { integer }
      Bytecode:
        0000000: 04b0
    
    	at java.base/java.lang.Class.forName0(Native Method)
    	at java.base/java.lang.Class.forName(Class.java:467)
    	…
    

    Verified with OpenJDK 25.0.4 · Linux x64; Broken.class (123 bytes) was written with the java.lang.classfile API, since javac never produces it

    One loader calls defineClass twice for FutureOutput
    defined Future once
    Exception in thread "main" java.lang.LinkageError: loader 'bytes' @251a69d7 attempted duplicate class definition for Future. (Future is in unnamed module of loader 'bytes' @251a69d7, parent loader 'platform')
    	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    	…
    

    Verified with OpenJDK 25.0.4 · Linux x64 (hash codes vary between runs)

    A user-defined loader tries to define java.lang.FutureOutput
    Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    	at java.base/java.lang.ClassLoader.preDefineClass(ClassLoader.java:847)
    	at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:960)
    	…
    

    Verified with OpenJDK 25.0.4 · Linux x64

  • Watching class loading in a real JVMHow do you see what a JVM loads, from where, and when?

    The JVM can write a line for every class it loads, checks and sets up. Reading that log is the most direct way to see this lab’s steps in a real program.

    java -Xlog:class+load,class+init -cp . Lifecycle (lines for Lifecycle and Config)Output
    [0.020s][info][class,load] Lifecycle source: file:/…/
    [0.020s][info][class,init] Start class verification for: Lifecycle
    [0.020s][info][class,init] End class verification for: Lifecycle
    [0.020s][info][class,init] 311 Initializing 'Lifecycle'(no method) (0x0000000076040000) by thread "main"
    main started
    [0.020s][info][class,load] Config source: file:/…/
    loaded Config; not initialized yet
    LIMIT = 42
    [0.026s][info][class,init] Start class verification for: Config
    [0.026s][info][class,init] End class verification for: Config
    [0.026s][info][class,init] 479 Initializing 'Config' (0x0000000076040210) by thread "main"
    <clinit> starts: count = 0, label = null
    count = 3
    label = ready
    

    Verified with OpenJDK 25.0.4 · Linux x64; timestamps, counters and addresses vary between runs

    jcmd <pid> VM.classloaders show-classes (while the plugin is loaded)Output
    +-- <bootstrap>
          |
          |               Classes: java.lang.Object
          |                        [Ljava.lang.Object;
          …
          |                        (825 classes)
          |
          |        Hidden Classes: java.lang.invoke.LambdaForm$MH/0x0000000043041000
          |                        (1 hidden class)
          |
          +-- "platform", jdk.internal.loader.ClassLoaders$PlatformClassLoader
                |
                +-- "app", jdk.internal.loader.ClassLoaders$AppClassLoader
                      |
                      |               Classes: Unload
                      |                        (1 class)
                      |
                      +-- "plugin", java.net.URLClassLoader
    
                                            Classes: com.example.plugin.Hello
                                                     (1 class)
    

    Verified with OpenJDK 25.0.4 · Linux x64; 823 of the bootstrap loader's class names are left out

Knowledge check

Check your understanding of class loading

5 questions. Answers are explained, right or wrong.

  1. Question 1

    Question 1 · Concept

    What identifies a class at run time?

  2. Question 2

    Question 2 · Concept

    The application loader is asked for java.util.logging.Logger. Which loaders take part?

  3. Question 3

    Question 3 · Order of events

    In which order does the JVM get a class ready?

  4. Question 4

    Question 4 · Predict the output

    A static block reads Config.count before static int count = 3; runs. What does it see?

  5. Question 5

    Question 5 · Concept

    Code that was compiled against Helper runs, but Helper.class has been deleted. What does the program get?

Read me

Accuracy and sources