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
Classobject. - 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 andstatic { }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 throwsExceptionInInitializerError).
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.