JVM Internals & Memory
Class Loading
Classes load lazily, on first use, through a delegating hierarchy of class loaders: bootstrap → platform → application. Loading, linking (verify/prepare/resolve), and initialization are distinct phases with precise ordering guarantees.
- Phases: load bytes → verify → prepare (defaults) → resolve (lazy) → initialize (static init runs)
- Parent delegation: a loader asks its parent first — core classes can't be spoofed
- A class's identity is (class loader, binary name) — same bytes in two loaders = two different classes
- Static initializers run once, under the JVM's own lock — the holder idiom exploits this
ClassNotFoundException(asked by name, absent) vsNoClassDefFoundError(present at compile, missing/failed at run)
$ java -verbose:class Main | head
[Loaded java.lang.Object from shared objects file]
[Loaded java.lang.String ...]
...
// Initialization is even lazier than loading:
class A { static { System.out.println("A initialized"); } }
Class<?> c = Class.forName("A", false, loader); // loaded, NOT initialized
A.someStatic(); // NOW "A initialized" printsInitialization triggers (JLS §12.4): first instance creation, static method call, non-constant static field access, Class.forName, or initialization of a subclass. The JVM guarantees exactly-once initialization under an internal lock — which is why the holder class idiom is thread-safe lazy loading with zero synchronization code of your own.
Custom class loaders override findClass and call defineClass(bytes) — the mechanism behind application servers (per-app isolation), plugin systems, hot reload, and bytecode instrumentation agents. Two loaders defining the same class name produce incompatible types: the infamous ClassCastException: com.acme.Foo cannot be cast to com.acme.Foo means two loaders each defined Foo.