RU | EN | DE

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+)

  1. Bootstrap ClassLoader — Native (in C++), loads the “core” JDK (java.base, etc.). Represented as null in Java.
  2. Platform ClassLoader (formerly ExtensionClassLoader before Java 8) — Loads platform modules of the JDK (not the base), extensions.
  3. 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); // null

Delegation Model (Parent Delegation)

The default algorithm for loadClass(name):

  1. Delegate to the parent (parent.loadClass(name)).
  2. 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

  1. Loading — searching for bytecode and creating Class<?> via defineClass.
  2. Linking = Verify (bytecode verification) + Prepare (allocation/initialization of static fields with zeros) + Resolve (resolving references to other types during access).
  3. 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 package SomeClass.
  • 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

SymptomWhen it occursHow to think
ClassNotFoundExceptionClass not found during name-based loading (Class.forName, loadClass)Not on the classpath/module path, different loader, incorrect TCCL
NoClassDefFoundErrorThe class was found earlier, but now the JVM can’t initialize/find it on referenceInitialization failed (ExceptionInInitializerError), or the class disappeared/is not visible from this loader
LinkageError (NoSuchMethodError, IncompatibleClassChangeError, ClassFormatError)Class/JAR version conflict, signature incompatibilityTwo versions of the same class in different loaders, “JAR hell”
ClassCastException “between identical names”The same class is loaded by different ClassLoadersIn 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 (or ALL-UNNAMED for the non-modular world).
  • Otherwise, you’ll get InaccessibleObjectException / IllegalAccessException when calling setAccessible(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, not loadClass (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/opens or JVM flags --add-opens for 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=false profile).
  • 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.