Class loading and initialization

Java · Expert · 14 min

Classpath conflicts, plugin systems, application servers and startup time all hinge on how classes are loaded.

The JVM loads classes lazily, the first time they are needed. Each class goes through three phases (JVMS chapter 5):

  • Loading — a class loader finds the bytes (from a JAR, directory, network, or generated) and creates a Class object.
  • Linking — verification checks the bytecode is type-safe and well-formed; preparation allocates static fields with default values; resolution turns symbolic references into direct ones, often lazily.
  • Initialization — runs the static initializer <clinit>: static field assignments and static { } blocks, in source order. It happens on first active use (creating an instance, calling a static method, reading a non-constant static field) and is guaranteed to run exactly once, thread-safely.

Class loaders form a hierarchy: the bootstrap loader (core java.base classes), the platform loader (other JDK modules), and the application (system) loader (your classpath or module path). Loaders use parent delegation: ask the parent first, and only load the class yourself if the parent can't. That stops applications from replacing java.lang.String.

A class's runtime identity is its name plus its defining loader. The same class loaded by two loaders gives two distinct types — the source of baffling ClassCastException: Foo cannot be cast to Foo in app servers and plugin systems.

Know the two errors apart:

  • ClassNotFoundException — a checked exception from explicit loading (Class.forName) when the class can't be found.
  • NoClassDefFoundError — the class was present at compile time but is missing at runtime, or its static initializer previously failed (the first attempt throws ExceptionInInitializerError).

Startup time is dominated by loading and verifying classes. Class Data Sharing (CDS, on by default for JDK classes; -XX:ArchiveClassesAtExit for your app) maps pre-parsed classes from an archive to speed it up.

Example

// Compile and run as a class (javac InitOrder.java && java InitOrder)
public class InitOrder {
    static class Config {
        static final String NAME = "app";            // compile-time constant: inlined, no init
        static final long STARTED;
        static {
            System.out.println("Config initializing");
            STARTED = System.nanoTime();
        }
        static void touch() { }
    }

    public static void main(String[] args) throws Exception {
        System.out.println(Config.NAME);             // app  (does NOT trigger initialization)
        Config.touch();                              // Config initializing  (exactly once)
        Config.touch();

        ClassLoader app = InitOrder.class.getClassLoader();
        System.out.println(app.getName() + " -> " + app.getParent().getName()); // app -> platform
        System.out.println(String.class.getClassLoader()); // null = bootstrap loader

        Class<?> c = Class.forName("java.util.ArrayList");
        System.out.println(c.getModule().getName());  // java.base
    }
}

Remember

  • Classes are loaded lazily: load, link (verify, prepare, resolve), initialize
  • Loaders delegate to their parent first; class identity = name + defining loader
  • NoClassDefFoundError means missing at runtime or a previously failed static initializer

Watch out for

  • Throwing from a static initializer: the first use gets ExceptionInInitializerError, every later use NoClassDefFoundError.

Check yourself

  1. Reading `Config.NAME`, where `static final String NAME = "app";`, from another class:

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/javase/specs/jvms/se21/html/jvms-5.html