Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 20 minutes
  • Examples run with Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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.

Class files feed the class loader subsystem, which defines classes in the runtime data areas; the execution engine runs them.Class filesfolders and JARs on the class path,the JDK's own modulesClass loader subsystemRuntime data areasExecution engineNative interface (JNI, FFM API)and native librariesApp loaderclass path, module path, JDK toolsPlatform loaderjava.sql, java.net.http and othersBootstrap loaderjava.base and the core modulesload → link → initialiseShared by all threadsOne set per threadInterpreterJITcompilersC1 and C2GarbagecollectorHeapobjects and arraysMethod area(Metaspace)classes, bytecodeCode cacheJIT machine codeJVM stacka frame per callpc registerthe currentinstructionNativemethod stackasks firstasks firstread bydefine classes inused bycalls

The parts of a running JVM

Text description of the diagram

The diagram runs from top to bottom in five blocks joined by arrows.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

Areas.java Java · Areas.java
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());
    }
}
The memory areas and threads of a fresh JVM Python · areas_session.py
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:

Loaders.java Java · Loaders.java
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();
    }
}
Which loader defined each class Python · loaders_session.py
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 no ClassLoader object, so getClassLoader() returns null for its classes, as the ClassLoader documentation says, and the program prints “bootstrap” for that null. It defines java.base and the other core modules.
  • Platform defined java.sql.Connection and java.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 defined com.sun.tools.javac.Main of the jdk.compiler module: 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 of Loaders[], says app, and [I, the class of int[], 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:

Twins.java Java · Twins.java
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]+", ""));
            }
        }
    }
}
plugin-src/Greeter.java Java · plugin-src/Greeter.java
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();
    }
}
Two loaders, one class file Python · twins_session.py
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.

ZIP & RAR Extractor Open a JAR in the browser and list its entries, to see whether a class or a resource such as tax-rates.properties is really inside it.

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:

Lifecycle.java Java · Lifecycle.java
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());
    }
}
Seven uses of a class, with the JVM's load and initialisation logs Python · lifecycle_session.py
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:

  1. new Config[4] loaded Config (an array class needs its element class) but ran nothing of it.
  2. The class literal Config.class gave the program the Class object without initialising the class.
  3. Class.forName with false as its second argument looks a class up without initialising it; the class was loaded already, so the log shows nothing.
  4. Config.VERSION is a constant variable, a static final field with a constant value, which javac copied into Lifecycle’s own bytecode: reading it touches Config not at all.
  5. Calling the static method name() initialised Config first: the <clinit> line comes before name returns. Only now was Config verified, which shows that HotSpot links a class lazily, just before it initialises it, as the JVM Specification allows.
  6. Derived.limit loaded both Base and Derived, but initialised only Base. The field is declared by Base, 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.
  7. new Derived() initialised Derived at last. Its superclass was initialised already; had it not been, the JVM would have initialised Base first.

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:

Rates.java Java · Rates.java
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);
                }
            }
        }
    }
}
A static initialiser that fails, then a run that works Python · rates_session.py
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 threw ArithmeticException. An exception that is not an Error is wrapped in an ExceptionInInitializerError, and the class is marked erroneous.
  • Attempt 2 found the class erroneous. The procedure then throws NoClassDefFoundError without 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.

Hex Viewer & File Inspector Open a .class file: the four bytes after CA FE BA BE are its version, 00 00 00 45 (69) for Java 25 and 00 00 00 47 (71) for Java 27.

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:

billing/app/shop/Billing.java Java · billing/app/shop/Billing.java
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);
                }
            }
        }
    }
}
billing/lib/shop/tax/TaxTable.java Java · billing/lib/shop/tax/TaxTable.java
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;
    }
}
Five runs of the billing program Python · failures_session.py
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.

  1. Question 1 of 7 Run 1: order 1 fails with ExceptionInInitializerError, order 2 with "NoClassDefFoundError: Could not initialize class shop.tax.TaxTable". What was wrong, and what fixes it?

    Choose one answer.

    Show the answer to question 1

    Answer: tax-rates.properties was not beside TaxTable.class, so the static initialiser of TaxTable threw; copy the file into the library's folder or JAR

    The first error's cause names the problem: "IllegalStateException: tax-rates.properties is not on the class path", thrown while TaxTable's static field RATES was initialised. That failure marks the class as erroneous, so order 2 gets NoClassDefFoundError without any code running, and its cause is the first error again. The session copied the file into out/lib/shop/tax/ before run 2, and both orders were priced.

  2. Question 2 of 7 Run 3: both orders fail with "NoClassDefFoundError: shop/tax/TaxTable", caused by "ClassNotFoundException: shop.tax.TaxTable". What was wrong?

    Choose one answer.

    Show the answer to question 2

    Answer: The class path named only out/app, so the library Billing was compiled against was missing at run time; add out/lib (or the library's JAR) to -cp

    Billing was compiled against TaxTable, so the JVM itself had to load the class when the call ran. No loader found it, and, as the JVM Specification says, loading then fails with a NoClassDefFoundError whose cause is the ClassNotFoundException. -cp out/app:out/lib, as in run 2, fixes it.

  3. Question 3 of 7 Run 4 stops at once: "ClassNotFoundException: shop.rules.Festive", thrown from Class.forName in Billing.main, and the exit status is 1. What was wrong?

    Choose one answer.

    Show the answer to question 3

    Answer: Billing looked up a class by the name given on the command line, and no loader has a class of that name; fix the name, or put the rule's classes on the class path

    ClassNotFoundException is what a lookup by name throws: Class.forName, ClassLoader.loadClass. Nothing in the program was compiled against shop.rules.Festive, so only the name was wrong or missing. Billing looks the rule up before its loop, outside the try that catches LinkageError (and ClassNotFoundException, a checked exception, is not a LinkageError anyway), so the exception ended main and the JVM exited with status 1.

  4. Question 4 of 7 Run 5: both orders fail with UnsupportedClassVersionError, "class file version 71.0", while this Java "only recognizes class file versions up to 69.0". What was wrong, and how is it fixed?

    Choose one answer.

    Show the answer to question 4

    Answer: The library was compiled by JDK 27, whose class files Java 25 cannot read; rebuild it for Java 25 (javac --release 25), or run the program on Java 27 or later

    Class-file version 69 is Java 25 and 71 is Java 27. A JVM reads class files of its own version and older, so the Java 25 JVM refused TaxTable while loading it. Recompiling Billing changes nothing; the library itself has to be built for the oldest Java release it must run on, which javac's --release option does on any newer JDK.

  5. Question 5 of 7 In the trace of Lifecycle, Config was loaded at step 1. At which numbered step was Config verified?

    Type a number.

    Show the answer to question 5

    Answer: 5

    The "Start class verification for: Lifecycle$Config" line comes after "5. Config.name()". HotSpot linked the class lazily, just before it initialised it, which the JVM Specification allows as long as a class is completely verified and prepared before it is initialised.

  6. Question 6 of 7 The static field limit is declared in Base. Which classes did reading Derived.limit initialise?

    Choose one answer.

    Show the answer to question 6

    Answer: Only Base

    A reference to a static field initialises only the class that declares it, even when the code names a subclass (JLS 12.4.1). The JVM still loaded Derived, because the reference names it, but Derived's static initialiser ran only at step 7, when an instance was created.

  7. Question 7 of 7 Put what happens to a class in the order the JVM does it.

    Give each item its position, from 1 (first).

    Show the answer to question 7

    Answer:

    1. Read the class file and create the class
    2. Verify the bytecode
    3. Give the static fields their default values
    4. Run the static initialisers

    Loading comes first, then linking (verification, then preparation, which gives every static field 0, false or null), and initialisation last, when <clinit> assigns the values written in the source. Resolution, the third part of linking, may happen later still, one reference at a time.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

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.
  • ClassNotFoundException comes from a lookup by name; NoClassDefFoundError means a class the code was compiled against is missing or failed to initialise, so look for the first error and its cause.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.