The idea: a class is loaded the first time it is needed

A Java program does not load all its classes at start. The JVM loads a class the first time running code needs it, in three phases: loading finds the class file and turns its bytes into a class, linking checks it and makes room for its static fields, and initialisation runs its static initialisers. Loading is done by a class loader, and the JDK has three of them, chained by a parent link.

Three class loaders stacked: bootstrap at the top reads java.base from lib/modules, platform in the middle reads java.sql and java.xml, application at the bottom reads the class path app.jar. A request for com.shop.App goes up from application to platform to bootstrap, null answers come back down, and the application loader finds App.class in app.jar
A request goes up the parent chain first; the loader that finally defines the class is the lowest one whose place holds it.

This page zooms into one part of How a Java Application Starts: what happens after main() is running, each time the code touches a new class. On the canvas the loaders are on the left, the life cycle of each class with its static fields in the middle, and the source of the classes in app.jar on the right. Pick a class and press loadClass() (find and define only) or First Use (the first real use, like calling App.run(): load, link and initialise), or run a demo.

The three class loaders

LoaderWhat it isWhere it looks
BootstrapC++ code inside the JVM. In Java it shows up as null (String.class.getClassLoader() == null).lib/modules: java.base, java.logging and the other core modules
PlatformA Java object, ClassLoader.getPlatformClassLoader(). Before Java 9 it was the extension loader.lib/modules: java.sql, java.xml, java.net.http …
Application (system)A Java object, ClassLoader.getSystemClassLoader(); it loads Main.The class path (-cp, -jar) and the module path

A class is identified by its name and the loader that defined it. Two loaders that define com.shop.App from the same bytes make two different classes, and casting one to the other throws ClassCastException. Inside one loader a class is defined once; the next request is answered from the loader's cache (findLoadedClass). Demo: load, link, initialise asks for App a second time.

Parent delegation

ClassLoader.loadClass first checks the cache, then asks the parent, and only if the parent returns nothing does it look in its own place. So for com.shop.App the request climbs to the bootstrap loader, which answers "not mine", the platform loader answers the same, and only then does the application loader read com/shop/App.class from app.jar (Demo: parent delegation). Loading App also loads its superclass Base, because a class cannot be created without it.

Delegation is what keeps the core classes safe and unique: a class from the class path can never replace java.lang.Object or java.lang.String. Since Java 9 the built-in loaders also know which module owns each package. A request for a class in java.lang goes straight to the loader of java.base, the bootstrap loader, and a request for java.sql.DriverManager goes to the platform loader, which reads it itself. The fake java/lang/String.class in app.jar is never even opened (Demo: JDK classes).

Load, link, initialise

Five phases left to right: Loading, then Linking with Verify, Prepare and Resolve, then Initialize. Under each, App's static fields: before preparation they do not exist, after preparation MAX 0, a 0, names null, b 0, after initialisation MAX 100, a 20, names [app], b 120
Preparation gives every static field its default value; only initialisation gives it the value written in the source.
  • Loading reads the bytes and parses them: magic number CAFEBABE, class file version (65 is Java 21), constant pool, fields and methods. The result lives in metaspace. No code of the class runs.
  • Verification proves the bytecode is type-safe with the help of each method's StackMapTable: every instruction gets operands of the right type, the operand stack cannot overflow, jumps stay inside the method. A failure is a VerifyError. Classes of the bootstrap loader are trusted and skip it.
  • Preparation creates the static fields with default values: 0, false, null. Since Java 8 they are stored in the class's java.lang.Class object on the heap.
  • Resolution turns symbolic references in the constant pool (java/util/ArrayList, java/lang/System.out) into direct pointers. The specification allows it early or late; HotSpot resolves each entry the first time an instruction uses it, which is why the canvas lights Resolve again in the middle of <clinit>.
  • Initialisation runs <clinit>, the method javac builds from all static field initialisers and static {} blocks. The JVM holds a lock on the class while it runs, so even with many threads it runs exactly once.

Static initialisation order

  1. The superclass is initialised first (Base before App). Interfaces are not, unless they have default methods.
  2. Fields that are compile-time constants (static final int MAX = 100) get their value from the ConstantValue attribute before any <clinit> code.
  3. Then field initialisers and static {} blocks run top to bottom, in the order they are written. In App, b = a + MAX is 120 because the block that doubles a comes before it.

Textual order is behind a classic puzzle. In class Counter { static Counter INSTANCE = new Counter(); static int count = 0; Counter() { count++; } } the constructor runs first and sets count to 1, and then the next line sets it back to 0. Put INSTANCE last, or don't write = 0, and count stays 1.

When initialisation happens

A class is initialised at its first active use: new, a call to a static method, reading or writing a static field that is not a constant, Class.forName(name), reflection, or the initialisation of a subclass. It is not initialised by loading it (loader.loadClass, Class.forName(name, false, loader)), by declaring a variable or an array of its type, or by App.class. Reading App.MAX from another class does not even load App: javac copied the constant 100 into the caller. This lazy start is what the holder idiom uses for a thread-safe singleton without locks: private static class Holder { static final Service INSTANCE = new Service(); }.

When it goes wrong

ErrorThrown whenDemo
ClassNotFoundException (checked)Code asks for a class by name (loadClass, Class.forName) and no loader in the chain finds itClass not found
NoClassDefFoundErrorA class that existed at compile time is missing at run time (caused by a ClassNotFoundException), or its initialisation failed earlierStatic initialiser throws (second use)
ExceptionInInitializerError<clinit> throws; the class is marked erroneous and never retriedStatic initialiser throws (first use)
UnsupportedClassVersionErrorThe class file was compiled for a newer Java than the running JVM—
VerifyError, LinkageErrorThe bytecode fails verification, or two loaders disagree about a class—

The Config case is common in real code: a static field reads a bad setting, the first caller sees ExceptionInInitializerError with the real cause, and every later caller only sees NoClassDefFoundError: Could not initialize class, so look for the first error in the log.

Custom class loaders

A custom loader extends ClassLoader (or uses URLClassLoader) and overrides findClass, which loadClass calls after the parents fail; it reads bytes from anywhere and calls defineClass. Application servers, Tomcat, OSGi and plugin systems give each application its own loader, so two apps can use different versions of one library. Some of them search their own place before the parent (child-first) for that reason. A class can be unloaded only when its loader, all its classes and all their objects are unreachable; that is how a server redeploys an app, and a leftover reference to an old loader is a classic metaspace leak (see Java garbage collection).

What the page leaves out

The module path and layers, -Xlog:class+load and -verbose:class output, CDS and AOT caches (on How a Java Application Starts), how javac builds the class file (on How a Java Program Is Compiled and Run), hidden classes and lambdas, loader constraints, the thread context class loader and ServiceLoader, interface initialisation details, and the exact locking protocol of initialisation (JVMS §5.5). Timing is not to scale.