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?
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
- SpecJVMS §5.3 Creation and Loading(opens in a new tab)
Defining and initiating loaders; loading with the bootstrap and user-defined loaders. · Java SE 25
- SourcesystemDictionary.cpp(opens in a new tab)
HotSpot's registry of loaded classes per class loader. · JDK 25 GA (tag jdk-25-ga)
- ToolThe jcmd command(opens in a new tab)
Diagnostic commands for a running JVM: VM.classloaders, GC.heap_info, VM.native_memory, Thread.print, GC.heap_dump. · JDK 25
- Spec
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’sMessage. 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)
- SpecJVMS §5.3 Creation and Loading(opens in a new tab)
Defining and initiating loaders; loading with the bootstrap and user-defined loaders. · Java SE 25
- SpecJVMS §5.3.4 Loading Constraints(opens in a new tab)
Constraints that keep type safety when classes are visible through different loaders. · Java SE 25
- Spec
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)
- SpecJVMS §5.3.4 Loading Constraints(opens in a new tab)
Constraints that keep type safety when classes are visible through different loaders. · Java SE 25
- SpecJVMS §5.4.3 Resolution(opens in a new tab)
Dynamically determines concrete values from symbolic references. · Java SE 25
- SourcesystemDictionary.cpp(opens in a new tab)
HotSpot's registry of loaded classes per class loader. · JDK 25 GA (tag jdk-25-ga)
- Spec
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: trueVerified with OpenJDK 25.0.4 · Linux x64 (hash codes vary between runs)
- SpecJVMS §5.3.6 Modules and Layers(opens in a new tab)
Run-time modules, layers and how class loaders map to them. · Java SE 25
- APIjava.lang.ModuleLayer(opens in a new tab)
The boot layer and layers created at run time; each module is mapped to a class loader. · Java SE 25
- JEPJEP 261: Module System(opens in a new tab)
Platform class loader, module-aware delegation, and the module path. · JDK 9
- Sourcemodule-loader-map.conf(opens in a new tab)
Which JDK modules are defined by the boot loader and which by the platform loader. · JDK 25 GA (tag jdk-25-ga)
- Spec
Initialization
Circular initializationWhat does a class see when initialization goes round in a circle?
AneedsBandBneedsA. The JVM does not loop forever: whenBasks forA, which is already being set up, it getsA’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 = 1Verified with OpenJDK 25.0.4 · Linux x64
- SpecJVMS §5.5 Initialization(opens in a new tab)
Executing <clinit>; the conditions that trigger initialization and its locking procedure. · Java SE 25
- SpecJLS §12.4.2 Detailed Initialization Procedure(opens in a new tab)
The initialization lock, recursive requests, textual order and the erroneous state. · Java SE 25
- Spec
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
- SpecJVMS §5.5 Initialization(opens in a new tab)
Executing <clinit>; the conditions that trigger initialization and its locking procedure. · Java SE 25
- SpecJLS §12.4.2 Detailed Initialization Procedure(opens in a new tab)
The initialization lock, recursive requests, textual order and the erroneous state. · Java SE 25
- Spec
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
Leftand needsRight, while thread two sets upRightand needsLeft, 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
- SpecJVMS §5.5 Initialization(opens in a new tab)
Executing <clinit>; the conditions that trigger initialization and its locking procedure. · Java SE 25
- SpecJLS §12.4.2 Detailed Initialization Procedure(opens in a new tab)
The initialization lock, recursive requests, textual order and the erroneous state. · Java SE 25
- ToolThe jcmd command(opens in a new tab)
Diagnostic commands for a running JVM: VM.classloaders, GC.heap_info, VM.native_memory, Thread.print, GC.heap_dump. · JDK 25
- Spec
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 doneVerified 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
- SpecJLS §12.7 Unloading of Classes and Interfaces(opens in a new tab)
Unloading is permitted, and only when the defining loader is reclaimable. · Java SE 25
- SourceclassLoaderData.hpp(opens in a new tab)
Per-loader metadata ownership; unit of class unloading. · JDK 25 GA (tag jdk-25-ga)
- Sourcemetaspace.hpp(opens in a new tab)
Metaspace: native memory for class metadata. · JDK 25 GA (tag jdk-25-ga)
- Spec
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
- SpecJLS §12.7 Unloading of Classes and Interfaces(opens in a new tab)
Unloading is permitted, and only when the defining loader is reclaimable. · Java SE 25
- SourceclassLoaderData.hpp(opens in a new tab)
Per-loader metadata ownership; unit of class unloading. · JDK 25 GA (tag jdk-25-ga)
- ToolThe jcmd command(opens in a new tab)
Diagnostic commands for a running JVM: VM.classloaders, GC.heap_info, VM.native_memory, Thread.print, GC.heap_dump. · JDK 25
- Spec
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 moreVerified 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.0Verified 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
- SpecJVMS §5.3.5 Deriving a Class from a class File Representation(opens in a new tab)
Format checks, superclass loading, and LinkageError / ClassFormatError conditions. · Java SE 25
- SpecJVMS §5.4.1 Verification(opens in a new tab)
Ensures the binary representation of a class is structurally correct. · Java SE 25
- SpecJVMS §4.10 Verification of class Files(opens in a new tab)
Type checking with StackMapTable frames and the constraints a class file must meet. · Java SE 25
- SpecJVMS §5.4.3 Resolution(opens in a new tab)
Dynamically determines concrete values from symbolic references. · Java SE 25
- SourceClassLoader.java(opens in a new tab)
loadClass: findLoadedClass, the parent, then findClass; preDefineClass forbids java.* names. · JDK 25 GA (tag jdk-25-ga)
- Spec
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 = readyVerified 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
- JEPJEP 158: Unified JVM Logging(opens in a new tab)
The -Xlog framework used for class, GC and other logs. · JDK 9
- ToolThe java command(opens in a new tab)
Launcher syntax, -Xss, -Xmx, -Xlog and the HotSpot -XX options. · JDK 25
- ToolThe jcmd command(opens in a new tab)
Diagnostic commands for a running JVM: VM.classloaders, GC.heap_info, VM.native_memory, Thread.print, GC.heap_dump. · JDK 25
- JEP
Knowledge check
Check your understanding of class loading
5 questions. Answers are explained, right or wrong.
Read me
Accuracy and sources
- SpecJVMS §5.3 Creation and Loading(opens in a new tab)
Defining and initiating loaders; loading with the bootstrap and user-defined loaders. · Java SE 25
- SpecJVMS §5.4 Linking(opens in a new tab)
Verification, preparation and (optionally lazy) resolution. · Java SE 25
- SpecJVMS §5.5 Initialization(opens in a new tab)
Executing <clinit>; the conditions that trigger initialization and its locking procedure. · Java SE 25
- SpecJLS §12.4.1 When Initialization Occurs(opens in a new tab)
The exact events that trigger class or interface initialization. · Java SE 25
- SpecJLS §12.7 Unloading of Classes and Interfaces(opens in a new tab)
Unloading is permitted, and only when the defining loader is reclaimable. · Java SE 25
- APIjava.lang.ClassLoader(opens in a new tab)
Built-in loaders, the delegation model, and getPlatformClassLoader / getSystemClassLoader. · Java SE 25
- SourceBuiltinClassLoader.java(opens in a new tab)
loadClassOrNull: module-aware lookup, then parent delegation, then class path. · JDK 25 GA (tag jdk-25-ga)
- SourceClassLoader.java(opens in a new tab)
loadClass: findLoadedClass, the parent, then findClass; preDefineClass forbids java.* names. · JDK 25 GA (tag jdk-25-ga)
- SourcesystemDictionary.cpp(opens in a new tab)
HotSpot's registry of loaded classes per class loader. · JDK 25 GA (tag jdk-25-ga)