Java Module 16 – JVM internals, memory, GC and performance
Class loading and JVM architecture
How the JVM is built and how classes reach it: the bootstrap, platform and app class loaders, delegation, initialisation and the errors of each step.
What you will learn
- Describe the bootstrap, platform and application class loaders and how they delegate
- Trace when a class is loaded, linked and initialised, from the JVM's own logs
- Diagnose ClassNotFoundException, NoClassDefFoundError and ExceptionInInitializerError from a log
Before you start
What you’ll learn · Before you start
What you will learn
- Describe the bootstrap, platform and application class loaders and how they delegate
- Trace when a class is loaded, linked and initialised, from the JVM's own logs
- Diagnose ClassNotFoundException, NoClassDefFoundError and ExceptionInInitializerError from a log
Before you start
On this page
A running JVM has three parts. The class loader subsystem finds class files and turns them into classes. The
runtime data areas hold what the program uses while it runs: the heap for objects, the method area for the
classes themselves, and a stack for each thread. The execution engine runs the bytecode with an interpreter, JIT
compilers and a garbage collector. Three class loaders come with the JDK: the bootstrap loader defines the core
classes, such as java.lang.String; the platform loader defines further Java SE modules, such as java.sql; and
the application loader, the app loader for short, defines your own classes from the class path. A loader asks its
parent before it looks for a class itself, and each loader keeps a set of classes of its own. A class is loaded when
the program first needs it, then linked and initialised, and each step that can fail has an error of its own:
ClassNotFoundException, NoClassDefFoundError, ExceptionInInitializerError.
The lesson on how Java runs followed one program through these steps. This one opens the machinery: which loader defines which class, why two classes with the same name can be different classes, exactly which uses initialise a class, and how to tell the loading errors apart in a log. Every example ran on Eclipse Temurin 25.0.4.1.
The parts of a running JVM
Text description of the diagram
The diagram runs from top to bottom in five blocks joined by arrows.
- Class files: the folders and JAR files on the class path, and the JDK's own modules. An arrow labelled "read by" leads to the next block.
- The class loader subsystem holds the three built-in loaders, one under the other. The app loader (the class path, the module path and the JDK's tools) asks the platform loader first; the platform loader (java.sql, java.net.http and other modules) asks the bootstrap loader first; the bootstrap loader defines java.base and the core modules. Under them, the steps every class goes through: load, link, initialise. An arrow labelled "define classes in" leads to the next block.
- The runtime data areas, in two columns. Shared by all threads: the heap, which holds objects and arrays; the method area, which HotSpot calls Metaspace, holding classes and their bytecode; and the code cache, holding machine code from the JIT compilers. One set per thread: the JVM stack, with a frame for each method call; the pc register, which points at the current instruction; and the native method stack. An arrow labelled "used by" leads to the next block.
- The execution engine, with three parts side by side: the interpreter, the JIT compilers C1 and C2, and the garbage collector. An arrow labelled "calls" leads to the last block.
- The native interface (JNI and the FFM API) and native libraries.
What a JVM holds before your code does anything
This program asks the JVM, through the java.lang.management API, which memory areas it manages and which Java
threads it started:
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryType;
import java.util.Comparator;
import java.util.List;
/** Lists the memory areas this JVM manages, and the Java threads it starts for a program that does nothing. */
public class Areas {
public static void main(String[] args) {
List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans().stream()
.sorted(Comparator.comparing((MemoryPoolMXBean p) -> p.getType()).thenComparing(MemoryPoolMXBean::getName))
.toList();
for (MemoryPoolMXBean pool : pools) {
long max = pool.getUsage().getMax();
System.out.printf("%-9s %-32s %s%n", pool.getType() == MemoryType.HEAP ? "heap" : "non-heap", pool.getName(),
max < 0 ? "no limit of its own" : "up to " + (max >> 20) + " MB");
}
System.out.println("classes loaded: " + ManagementFactory.getClassLoadingMXBean().getLoadedClassCount());
System.out.println("Java threads: " + Thread.getAllStackTraces().keySet().stream().map(Thread::getName).sorted().toList());
}
} from session import run
# The memory areas of a JVM started with no options on the track's build machine (48 GB of memory).
run("""
javac -d out Areas.java
java -cp out Areas
""") Output
# JDK in use: openjdk version "25.0.4.1" LTS $ javac -d out Areas.java $ java -cp out Areas heap G1 Eden Space no limit of its own heap G1 Old Gen up to 12288 MB heap G1 Survivor Space no limit of its own non-heap CodeHeap 'non-nmethods' up to 7 MB non-heap CodeHeap 'non-profiled nmethods' up to 116 MB non-heap CodeHeap 'profiled nmethods' up to 116 MB non-heap Compressed Class Space up to 1024 MB non-heap Metaspace no limit of its own classes loaded: 873 Java threads: [Common-Cleaner, Finalizer, Notification Thread, Reference Handler, Signal Dispatcher, main]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 areas_session.py
The Java Virtual Machine Specification describes six run-time data areas. HotSpot, the JVM in OpenJDK builds such as Temurin, gives them its own names, and the run shows them:
| Data area in the specification | What the run shows | Shared or per thread |
|---|---|---|
| Heap | G1 Eden Space, G1 Survivor Space and G1 Old Gen, the generations of the G1 garbage collector | Shared |
| Method area, with the run-time constant pools | Metaspace, plus the Compressed Class Space that holds class data the heap’s objects point to | Shared |
| (not in the specification) | Three CodeHeap areas, the code cache where the JIT compilers put machine code | Shared |
| JVM stacks, pc registers, native method stacks | Nothing: they live inside each thread, not in a memory pool | Per thread |
The specification deliberately leaves the layout open: it calls the method area logically part of the heap, yet does not mandate where it lives. HotSpot keeps this class metadata in native memory outside the heap, unlimited by default, as its garbage collection tuning guide explains, which is why the run reports Metaspace as non-heap with no limit of its own. The heap’s old generation may grow to 12,288 MB here, a quarter of the build machine’s 48 GB, which is the default maximum heap the JVM chose.
Two more numbers say a lot about start-up. The program had loaded 873 classes before its last line ran, most of them
for the management API it called. And five Java threads were already running beside main. Reference Handler,
Finalizer and Common-Cleaner belong to the machinery that reports unreachable objects and runs cleanup code;
Signal Dispatcher and Notification Thread connect the JVM to operating-system signals and to management tools.
The JIT compiler and garbage collector threads are not in the list: they are internal to the JVM, so Java code does
not see them.
The three built-in class loaders
Every Class object can say which loader defined it. This program asks seven classes, then walks up the chain of
parents from its own loader:
import java.util.List;
/** Which built-in class loader defined each class, and how the loaders are chained. */
public class Loaders {
public static void main(String[] args) throws Exception {
List<Class<?>> classes = List.of(
String.class, // java.base
java.sql.Connection.class, // java.sql
java.net.http.HttpClient.class, // java.net.http
Class.forName("com.sun.tools.javac.Main"), // jdk.compiler, a JDK tool
Loaders.class, // this program, from the class path
int[].class,
Loaders[].class);
for (Class<?> c : classes) {
String module = c.getModule().isNamed() ? c.getModule().getName() : "(unnamed module)";
System.out.printf("%-26s %-18s %s%n", c.getName(), module, nameOf(c.getClassLoader()));
}
System.out.println();
for (ClassLoader l = Loaders.class.getClassLoader(); l != null; l = l.getParent()) {
System.out.println(nameOf(l) + " delegates to its parent, " + nameOf(l.getParent()));
}
System.out.println("app is the system class loader: " + (ClassLoader.getSystemClassLoader() == Loaders.class.getClassLoader()));
}
/** The bootstrap loader has no ClassLoader object: getClassLoader() and getParent() return null for it. */
static String nameOf(ClassLoader l) {
return l == null ? "bootstrap" : l.getName();
}
} from session import run
run("""
javac -d out Loaders.java
java -cp out Loaders
""") Output
# JDK in use: openjdk version "25.0.4.1" LTS $ javac -d out Loaders.java $ java -cp out Loaders java.lang.String java.base bootstrap java.sql.Connection java.sql platform java.net.http.HttpClient java.net.http platform com.sun.tools.javac.Main jdk.compiler app Loaders (unnamed module) app [I java.base bootstrap [LLoaders; (unnamed module) app app delegates to its parent, platform platform delegates to its parent, bootstrap app is the system class loader: true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 loaders_session.py
- Bootstrap defined
String. The bootstrap loader is built into the JVM and has noClassLoaderobject, sogetClassLoader()returnsnullfor its classes, as theClassLoaderdocumentation says, and the program prints “bootstrap” for thatnull. It definesjava.baseand the other core modules. - Platform defined
java.sql.Connectionandjava.net.http.HttpClient, from Java SE modules outside the core set that JEP 261 assigns to the platform loader. - App defined
Loaders, from the class path, as a member of its unnamed module. It also definedcom.sun.tools.javac.Mainof thejdk.compilermodule: JEP 261, which brought modules in JDK 9, assigns the JDK’s tool modules to the application class loader. The app loader is also the system class loader, the default parent of any loader you create. - Arrays are not loaded by any loader: the JVM makes array classes itself, and an array class reports the loader of
its element type. That is why
[LLoaders;, the class ofLoaders[], says app, and[I, the class ofint[], has no loader at all.
The last three lines show the chain: app’s parent is platform, platform’s parent is bootstrap. The ClassLoader
documentation calls this the delegation model: when asked for a class, a loader usually asks its parent first and
searches its own places only if the parent finds nothing. The built-in loaders refine this, as JEP 261 describes: they
first check whether the class belongs to a named module that one of them defines, and if so that loader loads it; the
class path comes last. So a class of the JDK always comes from its JDK module, and a file called java/lang/String.class
on your class path is never used.
Same name, different class
The JVM Specification is precise about what makes a class itself: after it is created, a class is identified “not by
its name alone” but by its binary name together with its defining loader, the loader that actually read and
defined it. Two loaders that both read Greeter.class therefore make two different classes. This program builds two
URLClassLoaders named one and two over the same plugins folder:
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
public class Twins {
public static void main(String[] args) throws Exception {
URL[] plugins = { Path.of("plugins").toUri().toURL() };
ClassLoader app = ClassLoader.getSystemClassLoader();
try (URLClassLoader one = new URLClassLoader("one", plugins, app);
URLClassLoader two = new URLClassLoader("two", plugins, app)) {
Class<?> a = one.loadClass("Greeter");
Class<?> b = two.loadClass("Greeter");
System.out.println("same name: " + a.getName().equals(b.getName()));
System.out.println("same class: " + (a == b));
System.out.println("defined by: " + a.getClassLoader().getName() + " and " + b.getClassLoader().getName());
Object fromOne = a.getConstructor().newInstance();
try {
System.out.println(b.getMethod("greet", Object.class).invoke(null, fromOne));
} catch (InvocationTargetException e) {
// The identity hash after each loader's name (@1b6d3586) changes from run to run, so it is left out.
System.out.println(e.getCause().toString().replaceAll(" @[0-9a-f]+", ""));
}
}
}
} public class Greeter {
public String hello() {
return "hello from a Greeter defined by " + getClass().getClassLoader().getName();
}
/** Plugin code that receives an object and uses it as a Greeter: a checkcast instruction. */
public static String greet(Object o) {
return ((Greeter) o).hello();
}
} from session import run
# Greeter is compiled into plugins/, a folder that only the two URLClassLoaders read; then it is also put on the
# application class path.
run("""
javac -d plugins plugin-src/Greeter.java
javac -d out Twins.java
java -cp out Twins
java -cp out:plugins Twins
""") Output
# JDK in use: openjdk version "25.0.4.1" LTS $ javac -d plugins plugin-src/Greeter.java $ javac -d out Twins.java $ java -cp out Twins same name: true same class: false defined by: one and two java.lang.ClassCastException: class Greeter cannot be cast to class Greeter (Greeter is in unnamed module of loader 'one'; Greeter is in unnamed module of loader 'two') $ java -cp out:plugins Twins same name: true same class: true defined by: app and app hello from a Greeter defined by app
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 twins_session.py
In the first run, Greeter is only in plugins. Both loaders ask their parent, the app loader, which does not find
it, so each defines its own Greeter: the names are equal and the classes are not. An object made by loader one,
handed to code of loader two, fails the cast inside greet, and the message names the two loaders. In the second
run, Greeter is also on the class path. Now the parent finds it first, both loaders return the app loader’s class,
and everything works.
This is how a servlet container keeps two web applications apart, and how plugin systems load a new version of a
plugin without a restart: each gets a loader of its own. Tomcat’s class loader guide describes one loader per web
application, which even reverses the usual order and looks inside the application before asking its parent, except
for the JDK’s own classes and the APIs Tomcat implements. It is also where the strangest class-cast errors come from,
such as “Greeter cannot be cast to Greeter”: the same class file was defined twice, usually because a library sits
both in a shared folder and inside an application.
Loading, linking and initialising, traced
The JVM keeps three steps apart. Loading finds the class file and creates the class. Linking verifies the
bytecode, prepares the static fields with default values and resolves references to other classes. Initialising
runs the static initialisers, which javac compiles into a method called <clinit>. The Java Language Specification
(§12.4.1) lists exactly when a class is initialised: just before an instance is created, a static method declared by
the class is called, a static field declared by it is assigned, or one of its static fields that is not a constant
variable is read. Some reflective methods, such as the one-argument Class.forName, initialise a class too, and a
class’s superclasses are initialised before it. Apart from these, a class “will not be initialized under any other
circumstance”. This program tries seven uses, each announced by a numbered line:
public class Lifecycle {
static class Config {
static final String VERSION = "v3"; // a constant variable: javac copies "v3" into callers
static final long LOADED_AT = System.nanoTime(); // not a constant: reading it initialises Config
static {
System.out.println(" <clinit> of Config runs");
}
static String name() {
return "billing";
}
}
static class Base {
static int limit = 10;
static {
System.out.println(" <clinit> of Base runs");
}
}
static class Derived extends Base {
static {
System.out.println(" <clinit> of Derived runs");
}
}
public static void main(String[] args) throws Exception {
System.out.println("1. new Config[4]");
Config[] slots = new Config[4];
System.out.println("2. Config.class");
Class<?> type = Config.class;
System.out.println("3. Class.forName(\"Lifecycle$Config\", false, loader)");
Class.forName("Lifecycle$Config", false, Lifecycle.class.getClassLoader());
System.out.println("4. Config.VERSION = " + Config.VERSION);
System.out.println("5. Config.name()");
System.out.println(" returned " + Config.name());
System.out.println("6. Derived.limit");
System.out.println(" read " + Derived.limit);
System.out.println("7. new Derived()");
new Derived();
System.out.println(slots.length + " slots, " + type.getSimpleName());
}
} from session import run
# Two logs of the JVM, to standard output without decorations (":stdout:none"): class+load prints a line for every
# class it loads, class+init one when a class is verified and when it is initialised. grep hides the JDK's own
# classes; sed removes the memory address and the running number of each initialisation, which change between runs.
run(r"""
javac -d out Lifecycle.java
java -Xlog:class+load,class+init:stdout:none -cp out Lifecycle | grep -v -E "^(java|jdk|sun|com[.]sun)[.]|'(java|jdk|sun)/|for: (java|jdk|sun)/" | sed -E 's/ [(]0x[0-9a-f]+[)]//; s/^[0-9]+ (Initializing)/\1/'
""") Output
# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d out Lifecycle.java
$ java -Xlog:class+load,class+init:stdout:none -cp out Lifecycle | grep -v -E "^(java|jdk|sun|com[.]sun)[.]|'(java|jdk|sun)/|for: (java|jdk|sun)/" | sed -E 's/ [(]0x[0-9a-f]+[)]//; s/^[0-9]+ (Initializing)/\1/'
Lifecycle source: file:./out/
Start class verification for: Lifecycle
End class verification for: Lifecycle
Initializing 'Lifecycle'(no method) by thread "main"
1. new Config[4]
Lifecycle$Config source: file:./out/
2. Config.class
3. Class.forName("Lifecycle$Config", false, loader)
4. Config.VERSION = v3
5. Config.name()
Start class verification for: Lifecycle$Config
End class verification for: Lifecycle$Config
Initializing 'Lifecycle$Config' by thread "main"
<clinit> of Config runs
returned billing
6. Derived.limit
Lifecycle$Base source: file:./out/
Lifecycle$Derived source: file:./out/
Start class verification for: Lifecycle$Base
End class verification for: Lifecycle$Base
Initializing 'Lifecycle$Base' by thread "main"
<clinit> of Base runs
read 10
7. new Derived()
Start class verification for: Lifecycle$Derived
End class verification for: Lifecycle$Derived
Initializing 'Lifecycle$Derived' by thread "main"
<clinit> of Derived runs
4 slots, Config
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifecycle_session.py
Read the log against the seven numbered steps:
new Config[4]loadedConfig(an array class needs its element class) but ran nothing of it.- The class literal
Config.classgave the program theClassobject without initialising the class. Class.forNamewithfalseas its second argument looks a class up without initialising it; the class was loaded already, so the log shows nothing.Config.VERSIONis a constant variable, astatic finalfield with a constant value, whichjavaccopied intoLifecycle’s own bytecode: reading it touchesConfignot at all.- Calling the static method
name()initialisedConfigfirst: the<clinit>line comes beforenamereturns. Only now wasConfigverified, which shows that HotSpot links a class lazily, just before it initialises it, as the JVM Specification allows. Derived.limitloaded bothBaseandDerived, but initialised onlyBase. The field is declared byBase, and §12.4.1 says that a reference to a static field initialises only the class that declares it, even when the code names a subclass.new Derived()initialisedDerivedat last. Its superclass was initialised already; had it not been, the JVM would have initialisedBasefirst.
The difference between steps 1 and 5 is worth remembering when a program starts slowly: loading is cheap and happens early, while initialisation runs your code and happens at the first real use. The lesson on static members shows the order inside one class’s initialiser.
When initialisation fails
A static initialiser can throw. This rate table divides by the number of tax slabs, which is 0 unless the system
property slabs is set, and the program asks for a rate twice:
import java.util.Map;
public class Rates {
/** A rate table that fails to build: the divisor comes out as zero. */
static class Table {
static final int SLABS = Integer.getInteger("slabs", 0);
static final Map<String, Integer> PER_SLAB = Map.of("standard", 18 / SLABS);
static int rate(String name) {
return PER_SLAB.get(name);
}
}
public static void main(String[] args) {
for (int attempt = 1; attempt <= 2; attempt++) {
try {
System.out.println("rate: " + Table.rate("standard"));
} catch (Throwable t) {
System.out.println("attempt " + attempt + ": " + t);
for (Throwable cause = t.getCause(); cause != null; cause = cause.getCause()) {
System.out.println(" caused by: " + cause);
}
}
}
}
} from session import run
# The static initialiser of Rates.Table divides by the number of slabs, which is 0 unless -Dslabs is given.
run("""
javac -d out Rates.java
java -cp out Rates
java -Dslabs=2 -cp out Rates
""") Output
# JDK in use: openjdk version "25.0.4.1" LTS $ javac -d out Rates.java $ java -cp out Rates attempt 1: java.lang.ExceptionInInitializerError caused by: java.lang.ArithmeticException: / by zero attempt 2: java.lang.NoClassDefFoundError: Could not initialize class Rates$Table caused by: java.lang.ExceptionInInitializerError: Exception java.lang.ArithmeticException: / by zero [in thread "main"] $ java -Dslabs=2 -cp out Rates rate: 9 rate: 9
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rates_session.py
The two attempts fail differently, and the JVM Specification’s initialisation procedure (§5.5) explains both:
- Attempt 1 ran
<clinit>, which threwArithmeticException. An exception that is not anErroris wrapped in anExceptionInInitializerError, and the class is marked erroneous. - Attempt 2 found the class erroneous. The procedure then throws
NoClassDefFoundErrorwithout running anything: “Could not initialize class Rates$Table”. On JDK 25 that error carries the first one as its cause, with the thread it happened in, so the original division by zero is still in the log.
A class that failed to initialise stays broken for as long as its loader lives, which in most programs means until the
JVM stops. When a log is full of Could not initialize class, scroll up to the first ExceptionInInitializerError:
that is the real failure, and every later error is only its echo. The run with -Dslabs=2 shows the fix.
ClassNotFoundException or NoClassDefFoundError?
The two names sound alike and mean different things:
| Error | Who throws it | What it means |
|---|---|---|
ClassNotFoundException (checked) |
Code that asks for a class by its name: Class.forName, ClassLoader.loadClass |
No loader in the chain has a class of that name |
NoClassDefFoundError |
The JVM, when code that was compiled against a class runs | The class existed at compile time but cannot be loaded now (a ClassNotFoundException is its cause), or it failed to initialise earlier (“Could not initialize class”) |
ExceptionInInitializerError |
The JVM, the first time a static initialiser throws | Read its cause: that is the bug |
UnsupportedClassVersionError |
The JVM, while loading a class file | The class file was compiled for a newer Java release than the JVM that reads it |
NoClassDefFoundError’s documentation describes the usual case: the class was there when the running code was
compiled, and at run time the JVM can no longer find it. ClassNotFoundException, on the other hand, can happen to
code that was never compiled against the missing class at all: a name read from a
configuration file, a plugin named on the command line, a JDBC driver loaded by name. All of the errors except
ClassNotFoundException are subclasses of LinkageError, so code that must survive a broken class, such as a server
that handles each request on its own, can catch LinkageError and report it, as Billing does below, rather than
catching every Throwable.
Two JVM options show loading as it happens: -verbose:class prints each class as it is loaded, and
-Xlog:class+load does the same through the JVM’s logging framework, where it can be combined with other tags such as
class+init, as in the trace above.
Exercise: diagnose four failed runs
A small billing program, shop.Billing, uses a tax library, shop.tax.TaxTable, which reads its rates from a file
beside its class, tax-rates.properties. They were compiled into separate folders, as two JAR files would be, and
started five times. One run works; four fail in four different ways:
package shop;
import shop.tax.TaxTable;
/** Prices two orders. An order that fails is reported, and the next one is tried, as a server would. */
public class Billing {
public static void main(String[] args) throws Exception {
if (args.length > 0) {
Class<?> rule = Class.forName(args[0]); // a pricing rule named on the command line
System.out.println("rule: " + rule.getName());
}
long[] orders = {49_900, 1_250};
for (int n = 0; n < orders.length; n++) {
try {
System.out.println("order " + (n + 1) + ": tax " + TaxTable.taxOn(orders[n], "standard") + " cents");
} catch (LinkageError e) {
System.out.println("order " + (n + 1) + " failed: " + e);
for (Throwable cause = e.getCause(); cause != null; cause = cause.getCause()) {
System.out.println(" caused by: " + cause);
}
}
}
}
} package shop.tax;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Properties;
/** Tax rates in percent, read once from tax-rates.properties, which must sit next to this class. */
public class TaxTable {
private static final Properties RATES = load();
private static Properties load() {
try (InputStream in = TaxTable.class.getResourceAsStream("tax-rates.properties")) {
if (in == null) {
throw new IllegalStateException("tax-rates.properties is not on the class path");
}
Properties rates = new Properties();
rates.load(in);
return rates;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static long taxOn(long cents, String slab) {
return cents * Integer.parseInt(RATES.getProperty(slab)) / 100;
}
} from session import run
# Four runs of a small billing program that fail, and one that works. The library (shop.tax.TaxTable) and the
# application (shop.Billing) are compiled into separate folders, as two JAR files would be.
run("""
javac -d out/lib billing/lib/shop/tax/TaxTable.java
javac -d out/app -cp out/lib billing/app/shop/Billing.java
# Run 1
java -cp out/app:out/lib shop.Billing
cp billing/lib/shop/tax/tax-rates.properties out/lib/shop/tax/
# Run 2
java -cp out/app:out/lib shop.Billing
# Run 3
java -cp out/app shop.Billing
# Run 4
java -cp out/app:out/lib shop.Billing shop.rules.Festive
""", expect_failure=("java -cp out/app:out/lib shop.Billing shop.rules.Festive",))
# The library is rebuilt by a colleague with JDK 27 ...
run("""
javac -d out/lib27 billing/lib/shop/tax/TaxTable.java
cp billing/lib/shop/tax/tax-rates.properties out/lib27/shop/tax/
""", jdk="27")
# ... and the application still runs on JDK 25.
run("""
# Run 5
java -cp out/app:out/lib27 shop.Billing
""") Output
# JDK in use: openjdk version "25.0.4.1" LTS $ javac -d out/lib billing/lib/shop/tax/TaxTable.java $ javac -d out/app -cp out/lib billing/app/shop/Billing.java # Run 1 $ java -cp out/app:out/lib shop.Billing order 1 failed: java.lang.ExceptionInInitializerError caused by: java.lang.IllegalStateException: tax-rates.properties is not on the class path order 2 failed: java.lang.NoClassDefFoundError: Could not initialize class shop.tax.TaxTable caused by: java.lang.ExceptionInInitializerError: Exception java.lang.IllegalStateException: tax-rates.properties is not on the class path [in thread "main"] $ cp billing/lib/shop/tax/tax-rates.properties out/lib/shop/tax/ # Run 2 $ java -cp out/app:out/lib shop.Billing order 1: tax 8982 cents order 2: tax 225 cents # Run 3 $ java -cp out/app shop.Billing order 1 failed: java.lang.NoClassDefFoundError: shop/tax/TaxTable caused by: java.lang.ClassNotFoundException: shop.tax.TaxTable order 2 failed: java.lang.NoClassDefFoundError: shop/tax/TaxTable caused by: java.lang.ClassNotFoundException: shop.tax.TaxTable # Run 4 $ java -cp out/app:out/lib shop.Billing shop.rules.Festive Exception in thread "main" java.lang.ClassNotFoundException: shop.rules.Festive 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 shop.Billing.main(Billing.java:9) [exit status 1] # JDK in use: openjdk version "27" $ javac -d out/lib27 billing/lib/shop/tax/TaxTable.java $ cp billing/lib/shop/tax/tax-rates.properties out/lib27/shop/tax/ # JDK in use: openjdk version "25.0.4.1" LTS # Run 5 $ java -cp out/app:out/lib27 shop.Billing order 1 failed: java.lang.UnsupportedClassVersionError: shop/tax/TaxTable has been compiled by a more recent version of the Java Runtime (class file version 71.0), this version of the Java Runtime only recognizes class file versions up to 69.0 order 2 failed: java.lang.UnsupportedClassVersionError: shop/tax/TaxTable has been compiled by a more recent version of the Java Runtime (class file version 71.0), this version of the Java Runtime only recognizes class file versions up to 69.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 failures_session.py
For each failed run, name the error the JVM raised, say what was wrong and how you would fix it, then check yourself with the questions below. A hint for all four: read the first error of the run and its cause before anything else.
Check yourself: the four runs and the trace
7 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the parent delegation model of Java class loaders, and why does it exist?
A class loader that is asked for a class first asks its parent loader, and searches its own places only when the
parent cannot find the class. The built-in chain is the application loader, then the platform loader, then the
bootstrap loader, which has no parent. Since JDK 9 the built-in loaders also check first whether a class belongs to a
named module that one of them defines. The model gives each class one home: the core classes always come from the JDK,
so a stray java/lang/String.class on the class path can never replace the real one, and a class found by a parent is
shared by all the loaders below it instead of being defined again. A class is identified by its name and its defining
loader, so two loaders that define the same class file make two different classes, which is how containers keep
applications apart.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
After a deploy, a service logs one ExceptionInInitializerError at start-up and then thousands of "NoClassDefFoundError: Could not initialize class com.shop.TaxTable", one per request. The class file is in the JAR. What is going on, how do you find the cause, and how do you make the next failure obvious?
-
Senior to staff
A plugin host throws "ClassCastException: class com.acme.Rule cannot be cast to class com.acme.Rule (com.acme.Rule is in unnamed module of loader 'plugin-7'; com.acme.Rule is in unnamed module of loader 'app')". Explain how that is possible, how you confirm it, and how you fix and prevent it.
Key takeaways
- A JVM has a class loader subsystem, runtime data areas (heap, method area, code cache, and a stack for each thread) and an execution engine with an interpreter, JIT compilers and a garbage collector.
- The bootstrap loader defines the core modules, the platform loader further Java SE modules, and the app loader the class path and the JDK’s tools; each asks its parent first.
- A class is its name plus its defining loader: two loaders can make two different classes from one class file.
- Loading happens early and cheaply; linking and initialisation happen just before the first use that §12.4.1 lists, and a static field initialises only the class that declares it.
ClassNotFoundExceptioncomes from a lookup by name;NoClassDefFoundErrormeans a class the code was compiled against is missing or failed to initialise, so look for the first error and its cause.
References
- The Java Virtual Machine Specification, Java SE 25: Chapter 5, Loading, Linking, and Initializing (Oracle)
- The Java Virtual Machine Specification, Java SE 25: 2.5 Run-Time Data Areas (Oracle)
- The Java Language Specification, Java SE 25: 12.4.1 When Initialization Occurs (Oracle)
- ClassLoader (Java SE 25 API) (Oracle)
- NoClassDefFoundError (Java SE 25 API) (Oracle)
- ClassNotFoundException (Java SE 25 API) (Oracle)
- JEP 261: Module System (class loaders) (OpenJDK)
- The java Command (JDK 25 tool reference) (Oracle)
- HotSpot Virtual Machine Garbage Collection Tuning Guide (JDK 25): Other Considerations, Class Metadata (Oracle)
- Apache Tomcat 11: Class Loader How-To (The Apache Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress