Understanding JVM Class Loading: Mechanisms, Delegation, and Custom ClassLoaders
This article walks through the full lifecycle of a Java class—from loading, verification, preparation, and resolution to initialization, usage, and unloading—explains active versus passive references, the parent‑delegation model, JDK 8‑to‑17 module‑system changes, and shows how to write a custom ClassLoader for class isolation.
Class lifecycle – seven steps
A class or interface progresses through Loading → Verification → Preparation → Resolution → Initialization → Using → Unloading . Verification, preparation and resolution together form the linking phase. Loading, verification, preparation and initialization start in a fixed order; resolution may be deferred until after initialization to support runtime binding.
Loading phase
Obtain the binary byte stream of the class by its fully‑qualified name (e.g., com.example.Order).
Convert the byte stream into runtime data structures stored in the method area.
Create a java.lang.Class object on the heap as the entry point for accessing those structures.
The byte stream source is unrestricted – it may come from a JAR, a network download, a dynamically generated proxy, or a decrypted file, which enables custom ClassLoader implementations.
Verification
The JVM checks that the class file conforms to format, metadata, bytecode, and symbolic‑reference constraints, ensuring the class cannot compromise VM security.
Preparation
During preparation the JVM allocates memory for static fields and assigns them their default zero values. Example:
public class OrderCounter{</code><code> public static int total = 100;</code><code>}After preparation total is 0; the assignment to 100 occurs later in the initialization phase. An exception is static‑final compile‑time constants, which receive their final value already in preparation:
public class OrderCounter{</code><code> public static final int MAX = 1000;</code><code>}Resolution
Resolution replaces symbolic references in the constant pool with direct references (pointers or offsets) for classes, fields, methods, and interface methods.
Initialization
Initialization executes the class initializer <clinit>, which the compiler synthesizes by merging all static variable assignments and static blocks in source order. The initializer runs exactly once, is thread‑safe, and parent class initializers run before subclass initializers.
public class OrderConfig{</code><code> static int value = 1; // ①</code><code> static { value = 2; } // ②</code><code> static int reset = value; // ③ (value is already 2)</code><code>}Parent <clinit> executes before child.
If a class has no static blocks or static field assignments, the JVM may omit <clinit>.
The JVM guarantees that only one thread executes <clinit>, which underlies the thread‑safe static‑inner‑class singleton pattern.
public class OrderIdGenerator{</code><code> private OrderIdGenerator(){}</code><code> private static class Holder{</code><code> static final OrderIdGenerator INSTANCE = new OrderIdGenerator();</code><code> }</code><code> public static OrderIdGenerator getInstance(){</code><code> return Holder.INSTANCE; // triggers Holder initialization</code><code> }</code><code>}Active vs. passive references
Only six situations trigger class initialization (active references):
Bytecode instructions new, getstatic, putstatic, invokestatic (object creation, static field read/write, static method call).
Reflection via java.lang.reflect.
Initialization of a class whose superclass has not yet been initialized.
JVM startup initializing the class that contains main().
MethodHandle resolution (JDK 7+).
Default method execution causing interface initialization (JDK 8+).
All other references are passive and do not trigger initialization. Classic traps:
class Parent{ static int count = 10; static { System.out.println("Parent init"); }}</code><code>class Child extends Parent{ static { System.out.println("Child init"); }}</code><code>System.out.println(Child.count); // prints "Parent init" then 10 Parent[] arr = new Parent[10]; // no "Parent init" output class Const{ static final String NAME = "order"; static { System.out.println("Const init"); }}</code><code>System.out.println(Const.NAME); // prints "order" without "Const init"Class‑loader hierarchy (JDK 8)
Bootstrap (implemented in C++ as part of the VM) loads core libraries under <JAVA_HOME>/lib (e.g., rt.jar).
Extension ( sun.misc.Launcher$ExtClassLoader) loads classes from <JAVA_HOME>/lib/ext or directories specified by java.ext.dirs.
Application ( sun.misc.Launcher$AppClassLoader) loads classes from the user classpath.
System.out.println("order".getClass().getClassLoader()); // Bootstrap (null)</code><code>System.out.println(String.class.getClassLoader()); // null (Bootstrap)</code><code>System.out.println(OrderService.class.getClassLoader()); // AppClassLoader</code><code>System.out.println(OrderService.class.getClassLoader().getParent()); // ExtClassLoader</code><code>System.out.println(OrderService.class.getClassLoader().getParent().getParent()); // null (Bootstrap)Parent‑delegation model
A class‑loader first delegates a load request to its parent; only if the parent cannot find the class does the child attempt to load it. This guarantees a unique type system and protects core classes.
loadClass(name):</code><code> 1. if (findLoadedClass(name) != null) return it;</code><code> 2. try parent.loadClass(name); // delegate upward</code><code> 3. if parent fails, invoke findClass(name) to load locally.Breaking parent delegation
SPI (Thread‑Context ClassLoader) : Core libraries (Bootstrap) need to load implementations (e.g., JDBC drivers) that reside on the application classpath. Setting the thread‑context ClassLoader lets the parent “borrow” the child.
Tomcat : Each web application gets its own WebAppClassLoader that loads from WEB-INF/classes and WEB-INF/lib before delegating, providing isolation between apps.
Hot deployment (OSGi) : Modules receive separate ClassLoader instances; replacing a module discards its loader and loads a new one, turning the loader hierarchy from a tree into a mesh.
JDK 8 → JDK 17 module‑system changes
Extension loader removed : Replaced by the Platform ClassLoader . Hierarchy becomes Bootstrap → Platform → Application.
rt.jar and tools.jar disappeared : Core classes are now in lib/modules as separate .jmod modules.
ClassLoader is no longer URLClassLoader : Casting ClassLoader.getSystemClassLoader() to URLClassLoader throws ClassCastException on JDK 9+.
Parent delegation retained but with an extra module‑visibility check before delegating.
Custom ClassLoader example
public class FileClassLoader extends ClassLoader {</code><code> private final String baseDir; // directory containing .class files</code><code> public FileClassLoader(String baseDir){ this.baseDir = baseDir; }</code><code> @Override</code><code> protected Class<?> findClass(String name) throws ClassNotFoundException {</code><code> byte[] bytes = loadClassData(name);</code><code> if (bytes == null) throw new ClassNotFoundException(name);</code><code> return defineClass(name, bytes, 0, bytes.length);</code><code> }</code><code> private byte[] loadClassData(String name) {</code><code> String path = baseDir + "/" + name.replace('.', '/') + ".class";</code><code> try (InputStream in = new FileInputStream(path);</code><code> ByteArrayOutputStream out = new ByteArrayOutputStream()) {</code><code> byte[] buf = new byte[4096];</code><code> int len;</code><code> while ((len = in.read(buf)) != -1) out.write(buf, 0, len);</code><code> return out.toByteArray();</code><code> } catch (IOException e) { return null; }</code><code> }</code><code>}Using two loaders isolates classes with the same name:
FileClassLoader loaderA = new FileClassLoader("/app/v1");</code><code>FileClassLoader loaderB = new FileClassLoader("/app/v2");</code><code>Class<?> clazzA = loaderA.loadClass("com.example.Order");</code><code>Class<?> clazzB = loaderB.loadClass("com.example.Order");</code><code>System.out.println(clazzA == clazzB); // falseThis demonstrates that a class’s identity is the combination of its bytecode and the ClassLoader that defined it.
Key takeaways
Class lifecycle consists of seven steps; preparation gives static variables zero values unless they are compile‑time constants.
Initialization runs the synthesized <clinit> once, with parent classes first and JVM‑provided locking.
Only the six active‑reference cases trigger initialization; passive references such as child‑to‑parent static field access, array creation, and compile‑time constant usage do not.
Parent delegation ensures class uniqueness and blocks malicious core‑class replacement.
JDK 8 → 17 modularization rewrites the loader hierarchy (Extension → Platform), removes rt.jar, and replaces URLClassLoader with internal loader classes.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
