Introduction
The Java class loading mechanism was standardised in Java 1.2 (November 1998, JSR-12) with the introduction of the three-tier class loader hierarchy: Bootstrap, Extension, and Application. Before Java 1.2 the JVM had only a single built-in class loader, but the JSR-12 specification introduced parent-delegation so that core JDK classes could be loaded by the JVM's native loader while user code went through a chain of java.lang.ClassLoader subclasses. JDK 9 (September 2017, JEP 261 by Alan Bateman) reorganised the hierarchy: the Extension class loader became the Platform class loader and the Application class loader became the AppClassLoader, reflecting the new module system. For a Java learner, the Java class loading mechanism is the gateway to understanding how the JVM finds your classes, how dynamic proxies and frameworks like Spring and Hibernate load classes by name, and why ClassNotFoundException happens.
Understanding Java class loading also explains how application servers like Tomcat and WebLogic can host multiple web apps in one JVM without their classes colliding: each webapp gets its own ClassLoader, and the parent-delegation model guarantees that java.lang.String is always loaded exactly once by the Bootstrap loader. Interviewers ask about class loading because it sits between JLS-level Java code and JVM-level memory layout, and because every Java developer eventually hits a NoClassDefFoundError or ClassCastException that only makes sense once you understand class loader visibility.
What is Java Class Loading Mechanism?
The Java class loading mechanism is the runtime process by which the JVM reads a .class file, parses its bytecode, creates a java.lang.Class object in the method area, and makes the class available for instantiation. Loading is performed by instances of java.lang.ClassLoader, and the JVM ships with three built-in loaders arranged in a parent-delegation chain. The Bootstrap class loader (also called the primordial loader) is implemented in native C++ code inside the JVM and loads rt.jar (pre-Java 9) or the java.base module (Java 9+); it has no Java representation and getClassLoader() on java.lang.String returns null. The Extension class loader (renamed Platform class loader in Java 9) loads classes from the JDK extension directory (pre-Java 9) or the JDK modules that are not in java.base (Java 9+). The Application class loader (also called System class loader) loads classes from the user classpath.
Class loading is categorised as JVM runtime infrastructure, distinct from class file verification, preparation, and resolution which are the linking phases that follow loading. The defining feature of the Java model is parent-delegation: every user ClassLoader first asks its parent to load the class, and only attempts the load itself if the parent could not. This guarantees that java.lang.String is always loaded by the Bootstrap loader, which prevents a malicious application from substituting its own java.lang.String implementation. The trade-off is that two classes with the same fully-qualified name loaded by different sibling loaders are different runtime classes, and assigning between them throws ClassCastException.
How Java Class Loading Works - Step by Step
Walk through loading a single class com.example.MyApp from the application classpath. Suppose the Application loader has been asked to load com.example.MyApp by Class.forName("com.example.MyApp"). Here is exactly what happens.
Step 1: The Application loader (ClassLoader.getAppClassLoader() in Java 9+, sun.misc.Launcher$AppClassLoader in older JDKs) receives the loadClass("com.example.MyApp") call. It synchronises on the class-loading lock for this class name to guarantee that no other thread loads the same class concurrently.
Step 2: The Application loader calls findLoadedClass("com.example.MyApp") to check whether the class has already been loaded by this loader in this JVM session. If yes, it returns the existing Class object. Class loaders cache every class they define, so a class is loaded at most once per loader.
Step 3: If not yet loaded, the Application loader delegates upward by calling parent.loadClass("com.example.MyApp"). The parent is the Platform (or Extension) loader, which itself delegates upward to the Bootstrap loader. The Bootstrap loader searches java.base; if it finds the class there, it defines and returns it.
Step 4: If the Bootstrap loader does not find the class, the Platform loader searches its module path (or extension directory in pre-Java 9 JDKs). If found, it defines and returns it. Otherwise, control returns to the Application loader.
Step 5: The Application loader now calls its own findClass("com.example.MyApp"). It reads the bytes of com/example/MyApp.class from each classpath entry (a directory, JAR, or module), calls defineClass(bytes) to create the Class object, and resolves its direct references if resolveClass was requested.
Step 6: Before the class is visible to user code, the JVM performs verification (bytecode validity check), preparation (default static field initialisation), resolution (lazy resolution of symbolic references in the constant pool), and initialisation (running the static initialiser blocks). The class is now in the ready state and Class.forName returns.
Java Program for Java Class Loading Mechanism
package com.example.classloading;
import java.util.Scanner;
public class ClassLoadingDemo {
public static void printLoaderChain(Class<?> cls) {
System.out.println("Class: " + cls.getName());
ClassLoader cl = cls.getClassLoader();
if (cl == null) {
System.out.println(" Loaded by: Bootstrap (native loader, no Java object)");
return;
}
while (cl != null) {
System.out.println(" Loader: " + cl.getClass().getName());
cl = cl.getParent();
}
System.out.println(" Parent: Bootstrap (native loader)");
}
public static String describeLoader(ClassLoader cl) {
if (cl == null) {
return "Bootstrap (loads java.base, native C++)";
}
String name = cl.getClass().getName();
if (name.contains("Platform") || name.contains("ExtClassLoader")) {
return name + " (Platform/Extension loader)";
}
if (name.contains("AppClassLoader")) {
return name + " (Application/System loader)";
}
return name + " (custom loader)";
}
public static Class<?> loadByName(String className, boolean initialise)
throws Exception {
return Class.forName(className, initialise,
ClassLoader.getSystemClassLoader());
}
public static void main(String[] args) throws Exception {
System.out.println("=== Class Loader Hierarchy ===");
printLoaderChain(String.class);
System.out.println();
printLoaderChain(javax.sql.DataSource.class);
System.out.println();
printLoaderChain(ClassLoadingDemo.class);
System.out.println();
System.out.println("=== Loading a Class by Name ===");
Scanner sc = new Scanner(System.in);
System.out.print("Enter a fully-qualified class name: ");
String target = sc.hasNextLine() ? sc.nextLine().trim()
: "java.util.ArrayList";
if (target.isEmpty()) {
target = "java.util.ArrayList";
}
System.out.println("Loading: " + target);
Class<?> cls = loadByName(target, true);
System.out.println("Loaded: " + cls.getName());
System.out.println("Modifiers: " + Integer.toHexString(cls.getModifiers()));
System.out.println("ClassLoader: "
+ describeLoader(cls.getClassLoader()));
System.out.println("Number of declared methods: "
+ cls.getDeclaredMethods().length);
System.out.println();
System.out.println("=== Custom ClassLoader test ===");
ClassLoader app = ClassLoader.getSystemClassLoader();
ClassLoader plat = app.getParent();
ClassLoader boot = (plat == null) ? null : plat.getParent();
System.out.println("System loader: " + describeLoader(app));
System.out.println("Its parent: " + describeLoader(plat));
System.out.println("Its parent: " + describeLoader(boot));
System.out.println();
System.out.println("=== Loading without initialisation ===");
long t0 = System.nanoTime();
Class<?> lazy = Class.forName("java.util.HashMap", false,
ClassLoader.getSystemClassLoader());
long t1 = System.nanoTime();
System.out.println("Loaded (no init) in "
+ (t1 - t0) + " ns");
System.out.println("Static fields not yet run until reflection touches them");
}
}
Code Explanation - Line by Line
The printLoaderChain method takes a Class object and walks up its class loader by repeatedly calling getParent() until it returns null. The null return is the sentinel for the Bootstrap loader, which has no Java object representation; calling getClassLoader() on java.lang.String returns null because the Bootstrap loader loaded String. The describeLoader method maps the ClassLoader subclass name to a human-readable label and treats names containing Platform or ExtClassLoader as the Platform/Extension loader.
The loadByName method wraps Class.forName(className, initialise, loader). The three-argument overload is the only one that lets you control both which ClassLoader performs the load and whether the static initialiser runs immediately. Passing initialise=false returns the Class object with the static initialiser deferred until the first access to a static member; this is useful for tools like Hibernate that scan class hierarchies without invoking side-effecting initialisation logic.
The main method demonstrates three things: first, it walks the loader chain for String (Bootstrap), javax.sql.DataSource (Platform, because javax.sql is in the java.sql module which the Platform loader loads), and ClassLoadingDemo itself (Application, because the demo class is on the user classpath). Second, it asks the user for a class name and uses the three-argument Class.forName to load it explicitly through the system class loader. Third, it shows the explicit null Bootstrap sentinel by walking getParent() up the chain.
Interview Questions and Answers
Q1. What is the parent-delegation model and why does it matter?
Parent-delegation means every ClassLoader first asks its parent to load a class before attempting the load itself. It matters for security and consistency: it prevents a malicious classpath from replacing java.lang.String with a custom implementation, and it guarantees that a class is loaded by the highest-up loader that can see it. Without parent-delegation two ClassLoaders could load different versions of java.lang.String and assignments between them would fail at runtime.
Q2. What is the Bootstrap class loader and why does getClassLoader() return null for java.lang.String?
The Bootstrap (or primordial) class loader is implemented in native C++ code inside the JVM, not as a Java object. It loads the classes in java.base (or rt.jar in pre-Java 9 JDKs). Because it has no Java representation, Class.getClassLoader() returns null when the class was loaded by the Bootstrap loader. Test code can use cls.getClassLoader() == null as the canonical "loaded by Bootstrap" check.
Q3. What changed in the class loader hierarchy in Java 9?
JEP 261 in Java 9 (September 2017) renamed the Extension class loader to the Platform class loader, because the extension directory mechanism was replaced by the module system. The Platform loader now loads JDK modules that are not in java.base (such as java.sql, java.xml, java.logging). The Application class loader still loads user code but from the module path and classpath, and ClassLoader.getSystemClassLoader() now returns an instance of jdk.internal.loader.ClassLoaders$AppClassLoader.
Q4. What is the difference between ClassNotFoundException and NoClassDefFoundError?
ClassNotFoundException is a checked exception thrown by Class.forName and Loader.loadClass when a class name cannot be resolved at runtime by an explicit lookup; the class was never loaded. NoClassDefFoundError is an Error thrown when a class that was available at compile time is missing at runtime, or when a class's static initialiser fails after the class was loaded but before it was initialised. The two have similar names but different causes and recovery strategies.
Q5. What is the difference between loadClass and findClass and which should you override?
loadClass implements the parent-delegation algorithm: it checks findLoadedClass, delegates to the parent, then calls findClass. findClass is the extension point that reads the bytes from wherever your custom loader stores them and calls defineClass. You should override findClass (the contract since Java 1.2) and leave loadClass alone unless you deliberately want to break parent-delegation, for example in a hot-reload scenario or in a Tomcat webapp loader that loads servlet classes before parent classes.
Q6. What does Class.forName's initialise parameter actually control?
The initialise parameter, when false, defers running the static initialiser blocks of the loaded class until the first active use (instantiation, static method call, or static field access). When true, the static initialiser runs immediately during the forName call. Hibernate and JPA scanners use initialise=false so they can enumerate entity classes without triggering database connections or JNDI lookups in static blocks, and so that loading a class for metadata inspection never accidentally starts a thread pool.
Q7. You are building a plugin system where each plugin is a JAR loaded by its own URLClassLoader. Two plugins depend on different versions of the same library, and a class in plugin A assigns a value returned by plugin B's API. Walk through why this throws ClassCastException even though both versions have identical source code.
The cast fails because runtime class identity is defined by the (Class object, defining loader) pair, not by the bytecode. Plugin A's loader defines a class com.lib.Version loaded from plugin A's JAR, while plugin B's loader defines its own com.lib.Version loaded from plugin B's JAR; the JVM treats these as two distinct classes even though their bytecode is byte-identical. When plugin A receives a com.lib.Version instance from plugin B and tries to cast it, the check (targetClass.isAssignableFrom(actualClass)) returns false because the defining loaders differ. The fix is to push the shared library up the hierarchy (put it on the application classpath so both plugins see it via parent-delegation) or define a stable shared API interface loaded by a parent loader and pass instances through that interface.
Sample Input and Output
Input: $ java -cp . com.example.classloading.ClassLoadingDemo Enter a fully-qualified class name: java.util.ArrayList Output: === Class Loader Hierarchy === Class: java.lang.String Loaded by: Bootstrap (native loader, no Java object) Class: javax.sql.DataSource Loader: jdk.internal.loader.ClassLoaders$PlatformClassLoader Parent: Bootstrap (native loader) Class: com.example.classloading.ClassLoadingDemo Loader: jdk.internal.loader.ClassLoaders$AppClassLoader Loader: jdk.internal.loader.ClassLoaders$PlatformClassLoader Parent: Bootstrap (native loader) === Loading a Class by Name === Loading: java.util.ArrayList Loaded: java.util.ArrayList Modifiers: 401 ClassLoader: Bootstrap (loads java.base, native C++) Number of declared methods: 32 === Custom ClassLoader test === System loader: jdk.internal.loader.ClassLoaders$AppClassLoader (Application/System loader) Its parent: jdk.internal.loader.ClassLoaders$PlatformClassLoader (Platform/Extension loader) Its parent: Bootstrap (loads java.base, native C++) === Loading without initialisation === Loaded (no init) in 41230 ns Static fields not yet run until reflection touches them
The output shows the three-tier hierarchy in action. java.lang.String reports null for getClassLoader because the Bootstrap loader is native. javax.sql.DataSource loads via the Platform loader (Java 9+). The demo class itself loads via the Application loader, whose parent chain ultimately reaches the Bootstrap loader.
Common Mistakes to Avoid
Assuming getClassLoader() == null means the class is broken is wrong: null is the documented return for Bootstrap-loaded classes like java.lang.String, java.lang.Object, and java.util.HashMap. Code that treats null as an error will throw NullPointerException on the most fundamental JDK classes. Always check for null before dereferencing the class loader.
Overriding loadClass instead of findClass in a custom ClassLoader is the classic parent-delegation mistake: by overriding loadClass you bypass the delegation chain, which can break JDK security invariants and load java.lang.String from a custom source. Since Java 1.2 the contract is to override findClass only, leaving loadClass intact. Override loadClass only if you have a deliberate reason such as a servlet container's webapp-first policy.
Forgetting that Class.forName runs static initialisers by default causes subtle bugs. The single-argument Class.forName(name) calls the three-argument form with initialise=true, so database driver registration via Class.forName("com.mysql.cj.jdbc.Driver") works because the driver's static block self-registers with DriverManager. If you switch to the three-argument form with initialise=false, the driver never registers and DriverManager.getConnection fails.
Finally, comparing Class objects across loaders with == is fragile: two Class objects with the same name but loaded by different sibling loaders are not ==. Use Class.isAssignableFrom for safe cross-loader checks, and design plugin APIs around interfaces loaded by a shared parent loader.
Real-World Applications
Servlet containers like Tomcat and Jetty use class loaders heavily: each webapp gets a WebappClassLoader that loads WEB-INF/classes and WEB-INF/lib/*.jar, with the servlet API shared by the parent Common loader. The webapp-first delegation policy (Tomcat's default) checks the webapp's own repository before delegating, allowing webapps to ship their own versions of libraries that the container also uses. JBoss and WildFly go further with hierarchical loader domains per deployment.
OSGi frameworks (Eclipse Equinox, Apache Felix) generalise this model with one ClassLoader per bundle and explicit Import-Package / Export-Package metadata, allowing multiple versions of the same library to coexist in one JVM. Spring Boot's executable JAR uses a custom LaunchedURLClassLoader that loads BOOT-INF/classes and BOOT-INF/lib from inside a fat JAR via nested URLStreamHandler. JPA providers like Hibernate use Class.forName(name, false, loader) with initialise=false to scan for @Entity classes without triggering static initialisation.
Application servers, plugin systems (Eclipse, IntelliJ IDEA), JPMS module layers (java.lang.ModuleLayer), and dynamic languages on the JVM (Groovy, Kotlin scripting) all build on java.lang.ClassLoader. Even the JDK's own service loader (java.util.ServiceLoader) uses the caller's class loader to discover META-INF/services entries.
Conclusion
The Java class loading mechanism is the runtime process by which the JVM finds, parses, and defines classes through a parent-delegating three-tier ClassLoader hierarchy. The Bootstrap loader loads java.base, the Platform loader (formerly Extension) loads JDK modules, and the Application loader loads user code; the parent-delegation model guarantees that core classes are loaded once and consistently. Next study the class file verification and resolution steps, then the linking phase that follows loading, and finally the java.lang.invoke method handle machinery which lets you call methods dynamically without going through full class loading.
Comments
Comments
Post a Comment