Command Palette

Search for a command to run...

PHASE 14Advanced ~35 min· topic 1 of 6

Topic 14.1

Class Loading

In one line

Before a class can run, a class loader finds its bytes, the JVM links it (verify, prepare, resolve) and finally initialises it by running its static initialisers, exactly once and only on first active use. A class's identity at run time is its name *plus* the loader that defined it.

Think of it like this

A school library. When you ask for a book, the librarian first asks the head office whether they already hold it; only if they don't does she look on her own shelves. Once a book is on loan, she hands out the same copy every time you ask. And two libraries can both have a book called "Atlas" that are still different books. A class loader is that librarian, the head office is its parent loader, the loan record is the loader's cache, and "same title, different library" is why two classes with the same name can be different classes.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Class loader
An object that finds the bytes of a class (from a folder, a JAR, the network) and hands them to the JVM to turn into a class.
Bootstrap loader
The loader built into the JVM itself. It loads the core Java classes such as String and Object, and appears as null from Java code.
Parent delegation
The rule that a loader asks its parent for a class before trying to load the class itself.
Linking
The middle stage of getting a class ready: checking the bytecode is safe (verification), making room for static fields (preparation) and connecting names to real targets (resolution).
Initialisation
Running a class's static field initialisers and static blocks. Happens once, the first time the class is really used.
<clinit>
The hidden method the compiler builds from all static initialisers and static blocks of a class. The JVM calls it during initialisation.
Constant variable
A static final field of a primitive type or String whose value is known at compile time, such as static final int MAX = 10;. The compiler copies its value into the code that uses it.
Metaspace
Native memory (outside the heap) where the JVM keeps class metadata: method bytecode, field layouts, constant pools.
Class Data Sharing (CDS)
A JVM feature that saves already-parsed classes to an archive file and maps that file at start-up, so the JVM starts faster.

Step by step

01From .class file to running class

javac writes one .class file per class: a fixed format starting with the magic number 0xCAFEBABE, a version number (61 for Java 17, 65 for Java 21, 69 for Java 25), a constant pool of names and literals, and the bytecode of each method.

When your code first needs a class, the JVM asks a loader for it, then links and initialises it. The diagram shows the order. Verification is the step that makes Java memory-safe even for bytecode that didn't come from javac: it proves, for example, that no instruction pops an int and uses it as an object reference.

From .class file to running classdiagram
Rendering diagram…

02Three loaders, one delegation chain

Ask the app loader for java.sql.Connection and it delegates to the platform loader, which (on Java 9+) knows that the java.sql module is its own and defines it. Ask for java.lang.String and the request travels up to the bootstrap loader. Ask for your Main and both parents say "not mine", so the app loader reads it from the class path.

Since Java 9 the built-in loaders are module-aware: a loader knows which modules (and so which packages) belong to which loader, so for JDK packages it goes straight to the right loader instead of searching blindly (Topic 15.3 covers modules).

Main.javawhole filejava
// What ClassLoader.loadClass does, simplified from the JDK source
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);          // 1. already loaded by me?
        if (c == null) {
            try {
                c = (parent != null)
                        ? parent.loadClass(name, false)  // 2. ask the parent
                        : findBootstrapClassOrNull(name);
            } catch (ClassNotFoundException e) {
                // parent couldn't find it
            }
            if (c == null) c = findClass(name);       // 3. look myself
        }
        return c;
    }
}
Three loaders, one delegation chaindiagram
Rendering diagram…

03Watch classes load

The unified logging flag -Xlog:class+load (Java 9+, replacing -verbose:class, which still works) prints every class as it is loaded and where it came from. Even a hello-world loads several hundred JDK classes; most come from the CDS archive (shared objects file) rather than being parsed from scratch.

The output below is sample output: timestamps and counts differ on your machine.

terminal
$ java -Xlog:class+load:stdout Main.java | head -4 # sample output
── expected output ──
[0.006s][info][class,load] java.lang.Object source: shared objects file
[0.006s][info][class,load] java.io.Serializable source: shared objects file
[0.006s][info][class,load] java.lang.Comparable source: shared objects file
[0.006s][info][class,load] java.lang.CharSequence source: shared objects file

04Exactly when initialisation happens

Loading and initialising are different moments. A class can be loaded (its Class object exists) long before its static initialiser runs. The JLS lists the triggers precisely, and the first runnable example below tests each one.

The surprising case is the constant variable: static final int MAX = 10; is copied by javac into every class that reads it, so reading Config.MAX never touches Config at run time. That's also why changing a constant in a library and recompiling only the library leaves old values in callers that weren't recompiled.

Main.javawhole filejava
class Config {
    static final int MAX = 10;               // constant variable: inlined, no init
    static final Integer BOXED = 10;         // NOT a constant (not primitive/String): reading it initialises
    static final int COMPUTED = compute();   // NOT a constant (not known at compile time)
    static int compute() { return 10; }
}

05Initialisation is thread-safe, which gives a free lazy singleton

If two threads trigger initialisation of the same class at once, the JVM lets one run <clinit> and blocks the other until it finishes (JLS §12.4.2). The holder idiom uses this: the instance lives in a nested class that is initialised only when get() first touches it.

The same lock is a hazard: if <clinit> of class A waits for another thread that is initialising class B, while B's <clinit> needs A, the two threads deadlock inside class initialisation, and a thread dump shows them in RUNNABLE state waiting "on class initialization" (recent JDKs print this in jstack, Topic 14.5).

Main.javawhole filejava
final class Registry {
    private Registry() { }
    private static class Holder {                 // not initialised until get() runs
        static final Registry INSTANCE = new Registry();
    }
    static Registry get() { return Holder.INSTANCE; }  // first call initialises Holder, thread-safely
}

06When static initialisation fails

If <clinit> throws, the first caller gets ExceptionInInitializerError wrapping the real exception. The class is then marked as erroneous forever: every later use throws NoClassDefFoundError: Could not initialize class X, without the original cause in the message. In production logs you often see only the second error, thousands of times, while the first one (with the real cause) scrolled away at start-up. Recent JDKs (JDK 21, for example) attach a copy of the original error as the NoClassDefFoundError's cause (ExceptionInInitializerError: Exception java.lang.ArithmeticException: / by zero [in thread "main"]) to make this easier, but always search the log for the *first* error.

Don't confuse it with ClassNotFoundException, a checked exception from Class.forName or loadClass when the bytes can't be found at all.

07Custom loaders and the context class loader

You write a loader by extending ClassLoader (or using URLClassLoader) and overriding findClass, so delegation still works. Frameworks that load *your* classes from *their* code (JDBC drivers via ServiceLoader, JNDI, serialization libraries) can't use their own loader, which can't see your classes; they use the thread's context class loader, Thread.currentThread().getContextClassLoader(), which application servers set to the web app's loader.

Some containers invert delegation for web apps ("child-first": look in the app's WEB-INF/lib before the parent) so each app can use its own library versions. That's powerful and is exactly where Foo cannot be cast to Foo errors come from when the same library is visible in two places.

08Faster start-up with CDS

Since JDK 12 the JDK ships a default CDS archive for its own classes and uses it automatically. AppCDS (JDK 10) extends this to application classes: run once to record the classes used, dump an archive, and start later runs with it. JDK 19 added -XX:+AutoCreateSharedArchive, which creates and refreshes the archive automatically.

terminal
$ java -XX:ArchiveClassesAtExit=app.jsa -jar app.jar # training run, JDK 13+
java -XX:SharedArchiveFile=app.jsa -jar app.jar # later runs start faster
── expected output ──
(no special output; compare start-up times with and without the archive)

Try it yourself

  1. 1

    Find the trigger

    In "What triggers initialisation", change static final int MAX = 10; to static final Integer MAX = 10;. Predict where >> initialising Config now appears, then run it. (It moves up to step 1: a boxed Integer is not a constant variable, so reading it is an active use.)

    Now replace step 4 with Class.forName("Main$Config"); (the one-argument form). Predict, then run.

  2. 2

    Count the classes a program loads

    On your own JDK, run java -Xlog:class+load:file=classes.txt Main.java and count lines with wc -l classes.txt (or open it). Then search for source: jrt:/ versus source: shared objects file: the first group was parsed from the JDK's module image, the second came pre-parsed from CDS. Run again with -Xshare:off and compare start-up.

  3. 3

    Break the cast

    In "Same name, different loader", make Widget's constructor reachable with var ctor = fromA.getDeclaredConstructor(); ctor.setAccessible(true); Object w = ctor.newInstance(); and then try Widget x = (Widget) w;. Read the full ClassCastException message: it names both loaders.

Code & diagrams

What triggers initialisation, and what doesn't New tab

Steps 1 to 4 load Config at most, but never initialise it. Reading an inherited static field through Child initialises only the class that declares the field.

Sign in to run this example in your browser.

Expected output

1. read Config.MAX: 10
2. made a Config[3]
3. class literal: Config
4. Class.forName(name, false, loader) done
5. read Child.counter (declared in Config)
   >> initialising Config
6. read Child.own
   >> initialising Child
7. call Config.touch() again
sum 2
Same name, different loader, different class New tab

Both loaders read the very same Widget.class file, yet the JVM sees three different Widget classes. Core classes still come from the bootstrap loader, so String is shared.

Sign in to run this example in your browser.

Expected output

same name:              true
same Class object:      false
A's Widget == ours:     false
A asked again, cached:  true
our Widget instanceof A's: false
String via A is the JDK's: true
package-private access: denied (different runtime package)
A failed static initialiser poisons the class New tab

Only the first error names the real cause. Catching Throwable is for the demo only.

Sign in to run this example in your browser.

Expected output

attempt 1: java.lang.ExceptionInInitializerError
   message: null
   cause:   java.lang.ArithmeticException: / by zero
attempt 2: java.lang.NoClassDefFoundError
   message: Could not initialize class Main$Broken
A minimal custom class loader (fragment)java
import java.nio.file.*;

class DirLoader extends ClassLoader {
    private final Path root;

    DirLoader(Path root, ClassLoader parent) {
        super("plugins", parent);                // named loader (Java 9+)
        this.root = root;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Path file = root.resolve(name.replace('.', '/') + ".class");
        try {
            byte[] bytes = Files.readAllBytes(file);
            return defineClass(name, bytes, 0, bytes.length);   // JVM parses + verifies
        } catch (java.io.IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    }
}
// Override findClass, not loadClass, so parent delegation keeps working.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A class on the compile path but not the run path

Compile Main against a library JAR (javac -cp lib.jar Main.java) and then run it without the JAR on the class path (java Main).

terminal
$ java Main
── what you'll see ──
Exception in thread "main" java.lang.NoClassDefFoundError: com/acme/Greeter
at Main.main(Main.java:3)
Caused by: java.lang.ClassNotFoundException: com.acme.Greeter
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
...

Break #2

Defining your own java.lang class

Write package java.lang; public class Hacked { }, compile it, and load it from the class path.

terminal
$ java -cp . Main
── what you'll see ──
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

Myth vs fact

Myth

All classes are loaded when the program starts.

Fact

Classes are loaded on demand and initialised on first active use. A large app can run for minutes before touching some of its classes.

Myth

A class name uniquely identifies a class.

Fact

At run time a class is identified by its name plus its defining loader. The same name can exist several times in one JVM.

Myth

Loading a class runs its static block.

Fact

Loading and initialising are separate. Foo.class, Class.forName(name, false, loader) and declaring a Foo[] load without initialising.

Myth

Classes stay in memory forever.

Fact

Classes from custom loaders are unloaded when the loader and all its classes and instances become unreachable. Only classes of the built-in loaders are permanent.

When it breaks

Metaspace grows after every hot redeploy of a web app until OutOfMemoryError: Metaspace.

What you see

The server dies after N redeploys. A heap dump shows several copies of the app's classes, each held by an old WebappClassLoader.

Fix & prevent

Find what pins the old loader (a thread started by the app and never stopped, a ThreadLocal on a pooled thread, a JDBC driver registered in DriverManager, a static cache in a shared library). Stop threads and deregister drivers on shutdown; in the heap dump, follow "path to GC roots" from the old loader (Topic 14.5). Set -XX:MaxMetaspaceSize so the failure is loud and early rather than eating native memory.

Start-up logs one ExceptionInInitializerError and then thousands of NoClassDefFoundError: Could not initialize class ...Config.

What you see

Every request fails; the logs are flooded with the second error, which doesn't name the cause.

Fix & prevent

Search for the first error: its cause is the real problem (a missing environment variable, a bad config file). Avoid heavy work in static initialisers; load config explicitly at start-up and fail fast with a clear message.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    invokedynamic call sites (lambdas, string concatenation since Java 9, records' toString) are linked on first execution by a bootstrap method. Lambdas become hidden classes (Java 15, Lookup.defineHiddenClass), which are not discoverable by name and can be unloaded independently of their loader; they replaced the internal Unsafe.defineAnonymousClass.

  • ▸

    Verification uses StackMapTable attributes (class file version 50+, required from 51) so the verifier type-checks each method in one pass instead of inferring types. Old-style inference verification is only used for old class files. -Xverify:none / -XX:-BytecodeVerificationRemote are deprecated since JDK 13; don't use them to "speed up" start-up.

  • ▸

    Class loading is parallel-capable when a loader calls ClassLoader.registerAsParallelCapable(): locks are taken per class name (getClassLoadingLock) instead of on the whole loader. The built-in loaders are parallel-capable; custom loaders that aren't can become a start-up bottleneck.

  • ▸

    JDK 24 added ahead-of-time class loading and linking (JEP 483): a training run records which classes were loaded and linked, and later runs start with them already in that state, a further step beyond CDS that Project Leyden is building on. Check your JDK's documentation for the exact flags, which have been changing between releases.

Remember this

  1. 1

    A .class file is just bytes on disk or in a JAR. Turning it into a usable class happens in three phases that the Java Virtual Machine Specification (JVMS, chapter 5) names precisely. Loading: a class loader finds the bytes and the JVM parses them into an internal class structure (stored in Metaspace, Topic 14.2) and creates the java.lang.Class object you can see from Java. Linking: verification checks the bytecode is type-safe and can't corrupt memory, preparation allocates static fields and sets them to default values (0, null, false), and resolution turns symbolic names such as java/lang/String.length() into direct references, usually lazily, the first time each one is used. Initialisation: the JVM runs the class's <clinit> method, which is all the static field initialisers and static { } blocks in source order (Topic 4.6).

  2. 2

    Java has three built-in loaders since Java 9. The bootstrap loader is part of the JVM itself (written in C++, so it shows as null from Java) and loads the core modules such as java.base. The platform loader loads other Java SE and JDK modules such as java.sql. The application (system) loader, named app, loads your classes from the class path or module path. Before Java 9 the middle one was the extension loader and the class path was the only path; the ext and endorsed directories are gone.

  3. 3

    Loaders follow parent delegation: loadClass(name) first checks its cache, then asks its parent, and only if the parent can't find the class does it look itself. That's why nobody can replace java.lang.String with their own copy on the class path: the request always reaches the bootstrap loader first, and the JDK's String wins. The JVM also refuses to define classes in java.* packages from any loader other than the built-in ones, with a SecurityException ("Prohibited package name").

  4. 4

    A class's run-time identity is the pair (binary name, defining loader). Load com.shop.Order through two different loaders and you get two different Class objects whose instances can't be cast to each other: the famous ClassCastException: class Order cannot be cast to class Order. Application servers, plugin systems, OSGi and hot-reload tools all rely on this deliberately to keep apps apart. Package-private access also depends on it: two classes are in the same runtime package only if they have the same package name *and* the same loader.

  5. 5

    Initialisation is lazy and happens exactly once. The JLS (§12.4.1) lists the triggers: creating an instance, calling a static method, assigning a static field, reading a static field that is not a constant variable, some reflective calls such as Class.forName(name), and initialising a subclass (which initialises the superclass first). Things that do not trigger it: reading a static final compile-time constant (the compiler copied the value into your code), declaring a variable or an array of the type, using Foo.class, or Class.forName(name, false, loader). Reading a static field through a subclass (Child.counter where counter is declared in Parent) initialises only Parent. The JVM makes <clinit> thread-safe by holding an initialisation lock, which is why the holder-class idiom gives a lazy, thread-safe singleton for free.

  6. 6

    Classes are unloaded only when their defining loader becomes unreachable, together with every class it loaded and every instance of them; the garbage collector then frees their Metaspace. Classes from the bootstrap, platform and app loaders are never unloaded. That's why a leaked loader (a web app redeployed while an old thread or ThreadLocal still holds one of its objects) leaks *all* of its classes, and why Metaspace can fill up after many redeploys. Start-up cost is real too: loading, verifying and linking thousands of classes is a large part of JVM start-up, which Class Data Sharing (CDS) attacks by mapping pre-parsed classes from an archive file.

Explain it without notes

01

Walk through what happens between new Order() executing for the first time and the constructor running.

02

What is parent delegation and why does Java use it?

03

Why can ClassCastException: com.acme.Order cannot be cast to com.acme.Order happen?

04

What is the difference between ClassNotFoundException and NoClassDefFoundError?

Practice

01

Write a program that prints the loader names for Main, java.sql.Connection and java.lang.String (print bootstrap when the loader is null).

02

Demonstrate the holder idiom: print a line from the holder's static initialiser and show that it runs only when get() is first called, and only once.

03

Show that a static final String constant is inlined but a static final String computed by a method call is not.

Trade-offs

  • ↔

    Lazy loading and initialisation keep start-up fast and memory low, but move work (and failures) to the first request that needs a class; warm-up and readiness checks exist partly for this.

  • ↔

    Separate class loaders isolate apps and allow hot reload and plugin versions side by side, at the cost of cast errors, leaked loaders and more Metaspace.

  • ↔

    CDS and AOT archives speed up start-up but must be regenerated when the application or JDK changes, and add a build step.

Done when you can

  • Done when you can name and order the stages: loading, verification, preparation, resolution, initialisation.

  • Done when you can list what triggers class initialisation and what doesn't, including constant variables.

  • Done when you can explain parent delegation and the bootstrap, platform and app loaders.

  • Done when you can explain why the same class name can give a ClassCastException.

  • Done when you can diagnose NoClassDefFoundError: missing class vs failed static initialiser.