JVM Class Loading Mechanism
Class Loading Mechanism
The Java Virtual Machine (JVM) loads classes through a well-defined processs involving multiple phases: loading, linking, and initialization. A key aspect of this mechanism is the parent-delegation model, which governs how class loaders interact during class resolution.
Loading
During loading, the JVM locates the binary representation of a class or interface and creates an internal representation for it. This phase relies on ClassLoader instances, which follow a hierarchical delegation strategy.
Parent Delegation Model
- Check if already loaded: When a class loader receives a request to load a class, it first checks whether the class has already been loaded by itself or any of its ancestors. This check proceeds upward—from the current loader to the bootstrap class loader—to avoid duplicate loading.
- Delegate upward: If the class isn’t found, the request is delegated upward to the parent class loader. Only if all ancestors fail to locate the class does the current loader attempt to load it directly.
Example Code
public static void main(String[] args) {
// Application ClassLoader
System.out.println(new User().getClass().getClassLoader());
// Extension ClassLoader
System.out.println(new User().getClass().getClassLoader().getParent());
// Bootstrap ClassLoader (implemented in native code; returns null)
System.out.println(new User().getClass().getClassLoader().getParent().getParent());
// String is loaded by Bootstrap ClassLoader → prints null
System.out.println(String.class.getClassLoader());
}
Breaking the Parent Delegation Model
Although the parent delegation model promotes consistency and security, there are legitimate scenarios where it must be bypassed:
- Early JDK versions (pre-1.2): Before the parent delegation model was formalized in JDK 1.2, developers could override
loadClass(). To maintain backward compatibility, JDK 1.2 preserved this method while introducingfindClass()as the preferred extension point. OverridingloadClass()directly allows circumventing delegation logic. - Tomcat’s Web Application Isolation: Tomcat uses custom class loaders that prioritize loading classes from a web application’s own
WEB-INF/classesandWEB-INF/libdirectories before delegating upward. This enables multiple applications to use different versions of the same library without conflict. - SPI (Service Provider Interface) and Thread Context ClassLoader: Core Java APIs like JNDI are loaded by the bootstrap class loader but need to load provider implementations from the application classpath. Since the bootstrap loader can't access application classes, Java introduced the thread context class loader (
Thread.getContextClassLoader()), which typically defaults to the application class loader. This inversion breaks the strict delegation hierarchy. - OSGi Framework: OSGi implements a modular system with fine-grained class visibility and lifecycle control. Its class loaders often perform peer-to-peer lookups rather than strictly following the parent-first aproach, enabling dynamic module reloading and versioning.
- JDK 9+ Module System: With the introduction of the Java Platform Module System (JPMS), the extension class loader was renamed to the platform class loader. The
-Xbootclasspathandjava.ext.dirsoptions were deprecated. Core libraries are now packaged injimagefiles and accessed via the JRT filesystem. During loading, the platform class loader first checks if a class belongs to a named module; if so, it loads it directly instead of delegating upward.
Mitigation Strategy
To preserve delegation integrity, developers should override findClass() instead of loadClass(). The default implementation of loadClass() already incorporates the parent delegation logic and calls findClass() only when necessary.
Linking
After loading, the class undergoes linking, which consists of three sub-phases:
- Verification: Ensures the class file adheres to JVM specifications and doesn’t compromise security or integrity.
- Preparation: Allocates memory for static fields and initializes them to default values (e.g.,
0,false,null). - Resolution: Converts symbolic references (e.g., class/method names) into direct references (e.g., memory addresses).
Enitialization
The final phase executes the class’s static initializers and static field assignments explicitly defined in the source code. This occurs in a thread-safe manner the first time the class is actively used.
Common Interview Question
When is a static final field assigned its value?
The assignment timing depends on the field’s type and how it’s initialized:
- If the field is of a primitive type or
String, and is assigned a compile-time constant expression (e.g., literal or constant expression without method calls), the value is set during the preparation phase of linking. - Otherwise—such as when the initializer involves method calls, constructors, or non-constant expressions—the assignment occurs during the initialization phase inside the
<clinit>method.
public class InitializationTest {
public static int a = 1; // <clinit>
public static final int b = 10; // preparation
public static final int f = new Random().nextInt(); // <clinit>
public static Integer c = Integer.valueOf(100); // <clinit>
public static final Integer d = Integer.valueOf(100); // <clinit>
public static String s2 = "helloworld2"; // <clinit>
public static final String s0 = "helloworld0"; // preparation
public static final String s1 = new String("helloworld1"); // <clinit>
}
Rule of thumb: static final fields of primitive or String types are initialized during preparation only if their right-hand side is a compile-time constant. All other cases defer to the initialization phase.