Skip to main content

Java ClassLoaders Delegation & Metaspace Internals

In the Java Virtual Machine, classes are not loaded monolithically at startup. They are loaded lazily on-demand, linked, and materialized in native Metaspace memory through a tree of ClassLoaders.

Understanding the delegation model and Metaspace memory layout is vital for architecting plugin systems, modular web runtimes, and diagnosing critical OutOfMemoryError: Metaspace leaks.


1. The ClassLoader Delegation Hierarchy

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. Bootstrap ClassLoader (Native C++) β”‚
β”‚ β€’ Loads core JDK classes: java.base β”‚
β”‚ β€’ Represented as 'null' in Java API β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ (Delegates Parent First)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2. Platform ClassLoader (JDK 9+ Modules)β”‚
β”‚ β€’ Loads java.sql, java.xml, etc. β”‚
β”‚ β€’ Formerly Extension ClassLoader β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ (Delegates Parent First)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3. System / App ClassLoader (Classpath) β”‚
β”‚ β€’ Loads application classes from β”‚
β”‚ -classpath, -jar, and Main class β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ (Custom Delegation)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4a. Tomcat WebAppClassLoader β”‚ β”‚ 4b. OSGi Bundle ClassLoader β”‚
β”‚ (Child-First Delegation) β”‚ β”‚ (Peer-to-Peer Modular Graph) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The Parent-First Delegation Principle

Under standard parent-first delegation (ClassLoader.loadClass()):

  1. Check if the class is already cached in memory (findLoadedClass()).
  2. If not cached, delegate to the parent ClassLoader (parent.loadClass()).
  3. Only if all parents fail to find the class does the current loader invoke its own findClass() to read bytecode from disk or network.

The Child-First Exception (Web Servers & Containers)

In web servers (such as Apache Tomcat) and OSGi runtimes:

  • The Problem: WebApp A requires Spring 5.3, while WebApp B requires Spring 6.1. If the parent container loads Spring, both webapps are forced into a version clash.
  • Child-First Resolution: Tomcat's WebAppClassLoader overrides loadClass():
    1. Checks local /WEB-INF/classes and /WEB-INF/lib first.
    2. If the class exists locally, loads it immediately, isolating the web application from the container's libraries.
    3. Strict Boundary: Core java.* classes are never loaded child-first; they must always delegate to the Bootstrap ClassLoader for security.

2. Metaspace Architecture & Memory Allocation

In Java 8, the legacy PermGen (Permanent Generation) was replaced by Metaspace, which allocates class metadata in off-heap native memory:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ METASPACE NATIVE MEMORY ARCHITECTURE β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Compressed Class Space (-XX:CompressedClassSpaceSize=1G) β”‚ β”‚
β”‚ β”‚ β€’ Stores native Klass structures for 32-bit compressed klass pointers β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Non-Class Metaspace (Chunk Allocator) β”‚ β”‚
β”‚ β”‚ β€’ Method bytecode, Constant Pools, Annotations, Symbol Tables β”‚ β”‚
β”‚ β”‚ β€’ Allocated in arena chunks: 4KB (Small), 64KB (Medium), Specialized β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When Can a Class Be Unloaded?

Unlike heap objects that are reclaimed as soon as they become unreachable from GC roots, a class in Metaspace can only be unloaded if all three conditions are simultaneously met:

  1. Zero instances of the class exist on the Java heap.
  2. The java.lang.Class object is no longer referenced anywhere.
  3. The ClassLoader that loaded the class is completely unreachable and collected by GC.

3. The ThreadLocal Thread-Pool Metaspace Leak

The most prevalent Metaspace leak in enterprise Java stems from worker thread pools:

[Tomcat Worker Thread (Pooled, Never Dies)]
β”‚
β–Ό (ThreadLocal Map)
[ThreadLocal Key] ──► [MyContext Object]
β”‚
β–Ό
[WebAppClassLoader]
β”‚
β–Ό
[Pinned Metaspace Native Memory!]
  • The Hazard: A web request handler sets a ThreadLocal<MyContext> inside a worker thread. When the web application is redeployed, the application ClassLoader should be garbage collected.
  • The Root Cause: Because worker threads are pooled and never terminate, the ThreadLocal value on the worker thread retains a reference to MyContext, which points to its ClassLoader.
  • The Consequence: The entire ClassLoader, its classes, and all associated Metaspace native chunks are pinned forever, causing java.lang.OutOfMemoryError: Metaspace after 3 or 4 application redeployments.

4. Principal Architect Review Checklist

  • Thread Context ClassLoader Cleanliness: Are thread pools configured to reset Thread.currentThread().setContextClassLoader(null) or clean up ThreadLocal entries upon worker task completion?
  • Metaspace Sizing Guardrails: Is -XX:MaxMetaspaceSize explicitly configured alongside -XX:CompressedClassSpaceSize to prevent container memory exhaustion (OOMKilled)?
  • Child-First Isolation Rules: Do custom plugin or module ClassLoaders strictly delegate java.* and javax.* packages to the parent to maintain JVM security boundaries?
  • Metaspace Monitoring in JFR: Is jdk.MetaspaceSummary monitored to identify high water mark expansion rates?

πŸ“–
Track Page Progress0 / 635 Read
Knowledge Base Completion0%