Definition
- Bootstrap loader – native code, loads core
java.baseclasses likeObjectandString; represented asnull. - Platform loader – loads other standard JDK modules not in the bootstrap set.
- Application loader – loads your classpath or module path; returned by
ClassLoader.getSystemClassLoader(). - Parent-delegation – a loader asks its parent first; only loads itself if the parent fails. Core classes are always from Bootstrap.
- Class identity – fully-qualified name plus defining loader. Same name, different loaders, means distinct classes.
- Isolation – app servers and plugin systems use separate loaders so deployments do not clash.
Think of class loaders as a chain of librarians at National Bank of India. When a clerk requests a document, the junior librarian asks the senior, who asks the chief. Only if the seniors do not have it does the junior fetch his own copy. The bank identifies a document by both its title and which librarian issued it. Two documents titled "Fee Policy" from two different librarians are two genuinely different documents. That is how two versions of a plugin run side by side.
Understanding the Concepts
How does classloading work?
Classloading happens lazily: the JVM loads a class the first time it is actively used, for example when you create an instance, call a static method, or access a static field. The lifecycle has three stages:
- Loading – finds the class bytes and creates a
Classobject. - Linking – verification (bytecode safety), preparation (allocate static fields with defaults), resolution (resolve symbolic references).
- Initialization – runs static initializers and static field assignments, exactly once, thread-safe.
package com.bank.demo;
public class LoaderDemo {
public static void main(String[] args) {
// Bootstrap loader (null)
System.out.println("String loader: " +
String.class.getClassLoader()); // null
// Application loader
System.out.println("LoaderDemo loader: " +
LoaderDemo.class.getClassLoader());
// System class loader
System.out.println("System loader: " +
ClassLoader.getSystemClassLoader());
}
}
String loader: null
LoaderDemo loader: jdk.internal.loader.ClassLoaders$AppClassLoader@...
System loader: jdk.internal.loader.ClassLoaders$AppClassLoader@...
What is the class loader hierarchy?
The loaders form a chain:
- Bootstrap (root, native code) loads
java.baseclasses likejava.lang.Objectandjava.lang.String. - Platform loads other standard JDK modules.
- Application loads your application's classpath or module path.
How does parent delegation work?
When a loader is asked to load a class, it delegates to its parent first. Only if every ancestor fails does it try to load the class itself.
Two effects:
- Core classes are always loaded by Bootstrap, so you cannot replace
java.lang.Stringwith your own version. - Shared classes are loaded once by the parent and shared downward, avoiding duplicates.
package com.bank.demo;
public class DelegationDemo {
public static void main(String[] args) throws Exception {
// The standard loadClass flow:
// 1. Check if already loaded
// 2. Delegate to parent
// 3. If parent fails, call findClass
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = appLoader.loadClass("com.bank.demo.DelegationDemo");
System.out.println("Loaded by: " + clazz.getClassLoader());
}
}
Loaded by: jdk.internal.loader.ClassLoaders$AppClassLoader@...
Can two classes with the same name coexist?
Yes. A class's identity in the JVM is the pair of its fully-qualified name and the class loader that defined it. If two different loaders each load com.bank.FeePolicy, the JVM treats them as two distinct classes.
package com.bank.plugin;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
public class DualLoaderDemo {
public static void main(String[] args) throws Exception {
URL jar1 = Path.of("plugin-v1.jar").toUri().toURL();
URL jar2 = Path.of("plugin-v2.jar").toUri().toURL();
// Two separate class loaders
URLClassLoader loader1 = new URLClassLoader(
new URL[]{jar1}, null); // parent = null (bootstrap)
URLClassLoader loader2 = new URLClassLoader(
new URL[]{jar2}, null);
Class<?> class1 = loader1.loadClass("com.bank.FeePolicy");
Class<?> class2 = loader2.loadClass("com.bank.FeePolicy");
System.out.println("Same name: " +
class1.getName().equals(class2.getName())); // true
System.out.println("Same class: " +
(class1 == class2)); // false
System.out.println("Assignable: " +
class1.isAssignableFrom(class2)); // false
}
}
Same name: true
Same class: false
Assignable: false
They have separate Class objects, separate static fields, and are not assignment-compatible. Assigning an instance of one to a variable typed by the other throws ClassCastException, even though the source code is identical.
How do application servers use this for isolation?
Tomcat gives each deployed web application its own class loader. Two apps can bundle different versions of the same library without conflict. The web app loader checks its own WAR first for application classes, then delegates to the parent for core and server classes.
// Tomcat's per-app isolation:
// App1 bundles Jackson 2.14
// App2 bundles Jackson 2.17
// Each app's ClassLoader loads its own Jackson version
// They coexist as distinct classes in the same JVM
What is a class-loader leak and how does it happen?
If something outside a web app holds a reference to a class or instance from the app's loader, the loader cannot be garbage-collected on undeploy. This causes a class-loader leak that slowly exhausts metaspace.
Common causes:
- A
ThreadLocalin a shared thread pool holding an app-loaded object. - A JDBC driver registered in a parent-level
DriverManager. - A static cache in a shared library holding references to app classes.
// Example: ThreadLocal leak
// Shared thread pool thread has a ThreadLocal holding an app object
// On undeploy, the ThreadLocal keeps the app's ClassLoader alive
// Metaspace grows with each redeployment
// Fix: clear ThreadLocals on undeploy
// Tomcat's memory leak detection warns about this
How does the module system interact with classloading?
In the module system, classes belong to modules, and module boundaries control accessibility. The module graph adds another layer of encapsulation on top of class loaders. A module can exports packages to control which other modules can access its classes.
// module-info.java
module com.bank.core {
exports com.bank.model;
exports com.bank.service;
}
What about custom class loaders?
You create a custom class loader by extending ClassLoader and overriding findClass. This is used in plugin systems, hot-reloading frameworks, and sandboxed execution.
package com.bank.plugin;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class PluginClassLoader extends ClassLoader {
private final Path pluginDir;
public PluginClassLoader(Path pluginDir, ClassLoader parent) {
super(parent);
this.pluginDir = pluginDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name.replace('.', '/') + ".class";
Path classFile = pluginDir.resolve(fileName);
try {
byte[] bytes = Files.readAllBytes(classFile);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
What should a senior engineer know?
Understanding that identity equals name plus loader is the key to both using isolation deliberately and debugging when it goes wrong. The confusing error "FeePolicy cannot be cast to FeePolicy" always means two different loaders. Diagnosing class-loader leaks is a classic production task. When a loader and all its classes become unreachable, they can be garbage-collected, which is how undeploying an app frees its classes.
Visual Overview
Class Loader Hierarchy:
┌─────────────────────────────┐
│ Bootstrap Loader │ java.lang.Object, String
│ (native code, null) │ java.base module
└──────────┬──────────────────┘
│ parent
┌──────────▼──────────────────┐
│ Platform Loader │ java.sql, java.xml, etc.
└──────────┬──────────────────┘
│ parent
┌──────────▼──────────────────┐
│ Application Loader │ Your classpath / module path
└──────────┬──────────────────┘
│ child loaders
┌──────────▼───┐ ┌──────────▼───┐
│ App1 Loader │ │ App2 Loader │ (Tomcat per-app)
│ Jackson 2.14 │ │ Jackson 2.17 │
└──────────────┘ └──────────────┘
Class Identity:
name + loader = unique class
Same name + different loader = different class
Key Takeaway
Java classloading happens lazily and is performed by a hierarchy of class loaders. The Bootstrap loader loads core java.base classes. The Platform loader loads standard JDK modules. The Application loader loads your classpath. They cooperate through parent delegation: a loader asks its parent first and only loads a class itself if the parent cannot. This guarantees core classes always come from Bootstrap.
The crux is the identity rule. A class in the JVM is identified by the pair of its fully-qualified name and its defining class loader. Two classes with the same name loaded by different loaders are distinct types with separate Class objects, separate static fields, and are not assignment-compatible. This is the foundation of isolation: Tomcat gives each web app its own loader so they bundle different library versions without conflict.
At National Bank of India, Pankaj used this for a plugin platform where two business units shipped a fee-policy class with the same name but different logic. Each plugin got its own child class loader, so both classes coexisted as distinct types. The senior caution is class-loader leaks: if a ThreadLocal, a cache, or a registered JDBC driver holds a reference into an app's loader, that loader cannot be collected on undeploy, slowly exhausting metaspace.
Remember one sentence: a class's identity is its name plus its loader, so the same name from different loaders means different classes, which is how app servers and plugins achieve isolation.
---