Why Understand ClassLoader
- Correct operation of plugins, modules, SPI/ServiceLoader, DI frameworks.
- Diagnosing ClassNotFoundException / NoClassDefFoundError / LinkageError.
- Isolation of dependencies in application servers, OSGi, agenting, hot-reload.
- Conscious work with classpath/module-path and
getResource*.
Hierarchy of Loaders and Delegation Model
Core Loaders (JDK 9+)
- Bootstrap ClassLoader
— Native (in C++), loads the “core” JDK (
java.base, etc.). Represented asnullin Java. - Platform ClassLoader (formerly ExtensionClassLoader before Java 8) — Loads platform modules of the JDK (not the base), extensions.
- Application (System) ClassLoader — Loads application classes from the classpath (or module-path) — your JARs/classes.
Check in runtime:
ClassLoader app = ClassLoader.getSystemClassLoader();
ClassLoader platform = app.getParent();
ClassLoader bootstrap = platform.getParent(); // null → this is Bootstrap
System.out.println(app); // Application
System.out.println(platform); // Platform
System.out.println(bootstrap); // nullDelegation Model (Parent Delegation)
The default algorithm for loadClass(name):
- Delegate to the parent (
parent.loadClass(name)). - If the parent doesn’t find it — try to find it yourself (
findClass(name)).
This prevents replacing core classes of the core: your classes won’t “override” java.lang.String.
Loading Paths: classpath vs module-path
- Classpath: Before Java 8 (and still) — a list of directories/JARs.
Option:
-cp/CLASSPATH. - Module-path (Java 9+): Modular system JPMS.
Option:
--module-path. Control over export of packages (exports) and opening for reflection (opens).
If a class is “visible” on the module-path, but the package is not exported by the module — InaccessibleObjectException/IllegalAccess during reflection, or ClassNotFoundException when resolving module dependencies.
Class Lifecycle Phases in JVM
- Loading — searching for bytecode and creating
Class<?>viadefineClass. - Linking = Verify (bytecode verification) + Prepare (allocation/initialization of static fields with zeros) + Resolve (resolving references to other types during access).
- Initialization — executing
<clinit>: static initializers/static fields with values.
Initialization starts upon first active use: new, access to a static field/method, etc.
Thread Context ClassLoader (TCCL)
TCCL (Thread.currentThread().getContextClassLoader()) — the “contextual” loader, which frameworks use to find classes/resources “from the application’s side”, not “from the top”.
Application:
- ServiceLoader, XML parsers, loggers, JDBC drivers, SPI.
- In containers (e.g., servlet container), the container sets the TCCL for the web application.
Example:
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
URL res = tccl.getResource("config/app.yml");Resource Search: getResource vs getResourceAsStream
SomeClass.class.getResource("x.txt")— path relative to the packageSomeClass.SomeClass.class.getResource("/x.txt")— path relative to the root of the classpath.ClassLoader.getResource("x.txt")— always relative to the root.
Pattern:
try (var in = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream("config/app.yml")) {
// read thread
}Custom ClassLoader: when and how
When it’s needed:
- Plugin system / “tenants” with different library versions.
- Loading classes from a database/network/encrypted JARs.
- Dependency isolation, hot-reload.
Minimal example (proper parent delegation and custom findClass):
public class ByteArrayClassLoader extends ClassLoader {
private final Map<String, byte[]> defs;
public ByteArrayClassLoader(ClassLoader parent, Map<String, byte[]> defs) {
super(parent); // delegation to the parent
this.defs = defs;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = defs.get(name);
if (bytes == null) throw new ClassNotFoundException(name);
return defineClass(name, bytes, 0, bytes.length);
}
}
// Using:
Map<String, byte[]> code = Map.of("demo.Hello", compileBytes(...));
ClassLoader parent = Thread.currentThread().getContextClassLoader();
Class<?> helloCls = new ByteArrayClassLoader(parent, code).loadClass("demo.Hello");
Object hello = helloCls.getDeclaredConstructor().newInstance();Never break the “top-down” delegation without a very good reason — it’s easy to end up with “classloader hell.”
Common errors and how to tell them apart
| Symptom | When it occurs | How to think |
|---|---|---|
| ClassNotFoundException | Class not found during name-based loading (Class.forName, loadClass) | Not on the classpath/module path, different loader, incorrect TCCL |
| NoClassDefFoundError | The class was found earlier, but now the JVM can’t initialize/find it on reference | Initialization failed (ExceptionInInitializerError), or the class disappeared/is not visible from this loader |
LinkageError (NoSuchMethodError, IncompatibleClassChangeError, ClassFormatError) | Class/JAR version conflict, signature incompatibility | Two versions of the same class in different loaders, “JAR hell” |
| ClassCastException “between identical names” | The same class is loaded by different ClassLoaders | In the JVM, the type is {className, classLoader}. Two identical names → different types |
| Diagnostics: |
System.out.println(obj.getClass().getClassLoader());
// or
System.out.println(SomeClass.class.getClassLoader());Containers and isolation (Tomcat/Jetty/Spring Boot)
- Each web application has its own ClassLoader. This allows different library versions to coexist on the same server.
- The container sets the TCCL to the web application’s ClassLoader before invoking a servlet or filter.
- Many SPIs (JDBC drivers, JAXB, JAXP, loggers) look up classes via TCCL. If TCCL isn’t set, you’ll get a
ClassNotFoundException.
Fat JAR (Spring Boot):
- Contains a single “launcher” with its own JAR resolver → a dedicated ClassLoader stack.
- When debugging classes “from JAR inside JAR,” pay attention to
LaunchedURLClassLoader.
Module system (Java 9+) and reflective access
exports: makes a package visible for compilation/linking to other modules.opens: exposes a package for reflection (e.g., Hibernate, Jackson).- JVM options to bypass restrictions:
--add-opens module/package=targetModule(orALL-UNNAMEDfor the non-modular world). - Otherwise, you’ll get
InaccessibleObjectException/IllegalAccessExceptionwhen callingsetAccessible(true), even if the class is on the path.
Service Provider Interface (SPI) and ServiceLoader
public interface PaymentGateway { void pay(); }
// META-INF/services/com.example.PaymentGateway
// contains a line with the FQN of the implementation: com.example.impl.StripeGateway
ServiceLoader<PaymentGateway> loader =
ServiceLoader.load(PaymentGateway.class, Thread.currentThread().getContextClassLoader());
for (PaymentGateway gw : loader) {
gw.pay();
}⚠️TCCL matters — otherwise, the ServiceLoader may not “see” providers from your ClassLoader.
Best practices
- Respect upward delegation: override
findClass, notloadClass(unless you truly know what you’re doing). - Log the ClassLoader on errors: where the class/resource was searched.
- Use TCCL in frameworks/SPIs:
Thread.currentThread().getContextClassLoader(). - Diagnose conflicts: check whether the same class is being loaded by different loaders.
- Resource loading:
ClassLoader.getResource*— path from the root;Class#getResource*— relative to the package. - Modules: plan
exports/opensor JVM flags--add-opensfor reflection. - Avoid multiple versions of the same JAR on one path: “JAR hell” → LinkageError.
- Close
URLClassLoader(close()) if you create temporary loaders (hot reload/plugins) — otherwise you’ll get memory leaks. - In Spring Boot, consider the launcher’s custom loader; for native tests, unpack the “fat JAR” to a standard classpath (
repackage=falseprofile). - Plugin isolation: use a separate ClassLoader per plugin with a parent for the API; minimize overlap.
Mini example: diagnosing “the class exists but isn’t visible”
Symptom: NoClassDefFoundError: Lcom/x/Foo; when invoking library code.
Step-by-step:
System.out.println(Foo.class.getClassLoader());
// Where lies?
System.out.println(Foo.class.getProtectionDomain().getCodeSource().getLocation());
// Who call?
for (var el : Thread.currentThread().getStackTrace())
System.out.println(el);
// check TCCL
System.out.println(Thread.currentThread().getContextClassLoader());
// check dublications
Enumeration<URL> urls =
Thread.currentThread().getContextClassLoader().getResources("com/x/Foo.class");
while (urls.hasMoreElements()) System.out.println(urls.nextElement());If there are multiple paths — you have duplicates. If there are different loaders — it’s an isolation conflict.