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()):
- Check if the class is already cached in memory (
findLoadedClass()). - If not cached, delegate to the parent ClassLoader (
parent.loadClass()). - 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
WebAppClassLoaderoverridesloadClass():- Checks local
/WEB-INF/classesand/WEB-INF/libfirst. - If the class exists locally, loads it immediately, isolating the web application from the container's libraries.
- Strict Boundary: Core
java.*classes are never loaded child-first; they must always delegate to the Bootstrap ClassLoader for security.
- Checks local
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:
- Zero instances of the class exist on the Java heap.
- The
java.lang.Classobject is no longer referenced anywhere. - The
ClassLoaderthat 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
ThreadLocalvalue on the worker thread retains a reference toMyContext, which points to itsClassLoader. - The Consequence: The entire ClassLoader, its classes, and all associated Metaspace native chunks are pinned forever, causing
java.lang.OutOfMemoryError: Metaspaceafter 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 upThreadLocalentries upon worker task completion? - Metaspace Sizing Guardrails: Is
-XX:MaxMetaspaceSizeexplicitly configured alongside-XX:CompressedClassSpaceSizeto prevent container memory exhaustion (OOMKilled)? - Child-First Isolation Rules: Do custom plugin or module ClassLoaders strictly delegate
java.*andjavax.*packages to the parent to maintain JVM security boundaries? - Metaspace Monitoring in JFR: Is
jdk.MetaspaceSummarymonitored to identify high water mark expansion rates?
