The JVM’s ClassLoader subsystem is failing to unload classes, leading to an out-of-memory error because the garbage collector can’t reclaim the memory occupied by these unloaded classes.
The most common culprit is the ThreadLocal variable holding a reference to a ClassLoader instance. When a thread finishes its work but remains active (e.g., in a thread pool), its ThreadLocal variables are not cleared. If the ThreadLocal holds a reference to a ClassLoader, that ClassLoader (and all the classes it loaded) becomes ineligible for garbage collection, even if the application logic intended for that ClassLoader to be discarded.
Diagnosis:
-
Heap Dump Analysis: Take a heap dump when the OOM error is imminent or has occurred. Use tools like Eclipse Memory Analyzer (MAT) or VisualVM. Look for large numbers of
java.lang.Classobjects and investigate whichClassLoaderinstances are holding onto them. Specifically, search forThreadLocalMapentries that point toClassLoaderinstances.- Command (MAT):
File -> Open Heap Dump, thenDominator TreeorPath to GC Rootswithexclude weak/soft references. - Fix: Identify the
ThreadLocalvariable responsible. Ensure that theThreadLocal’sremove()method is called in afinallyblock or when the thread’s task is completed, especially in long-lived threads like those in application server thread pools. - Why it works: Explicitly nullifying the
ThreadLocalreference breaks the chain of references from the thread to theClassLoader, allowing theClassLoaderand its loaded classes to be garbage collected.
- Command (MAT):
-
Custom ClassLoader Stays Alive: If your application dynamically loads classes using custom
ClassLoaders (e.g., for plugin systems, hot-reloading), theseClassLoaders might not be getting garbage collected. This often happens if they are still referenced by static fields, singletons, or external objects that outlive their intended scope.- Diagnosis: In your heap dump, look for instances of your custom
ClassLoaderand trace theirPath to GC Roots. Pay close attention to static fields. - Fix: Ensure that custom
ClassLoaderinstances are properly dereferenced when they are no longer needed. If they are managed by a framework, check the framework’s lifecycle management for these objects. Avoid holding static references to them. - Why it works: Removing the last strong reference to the custom
ClassLoadermakes it eligible for garbage collection, which in turn allows the classes it loaded to be collected.
- Diagnosis: In your heap dump, look for instances of your custom
-
Application Server Thread Pools: Application servers (like Tomcat, JBoss, WebSphere) manage their own thread pools. If an application deployed within the server leaks a
ClassLoader(e.g., via aThreadLocalas mentioned above), and the server reuses the thread for another deployment or task, the leakedClassLoaderfrom the previous context can persist.- Diagnosis: Use
jstackto examine thread dumps. Look for threads in the server’s pool that are holding ontoClassLoaderinstances, especially if you see multiple instances of the same class loaded by differentClassLoaders. - Fix: This is often a consequence of other leaks. The primary fix is to ensure all components (servlets, filters, listeners, application code) properly clear
ThreadLocals and dereferenceClassLoaders when they are no longer needed, particularly during application undeployment or thread termination. Some servers have specific undeployment cleanup mechanisms that might need to be overridden or fixed. - Why it works: By ensuring threads are clean upon task completion or context change, the server can correctly manage its resources and avoid carrying over leaked class metadata between application lifecycles.
- Diagnosis: Use
-
JNDI Context Leak: Applications using JNDI (Java Naming and Directory Interface) can sometimes leak
ClassLoaders. If aClassLoaderis used to look up JNDI resources and theNamingEnumerationorDirContextis not properly closed, it might hold a reference to theClassLoader.- Diagnosis: Analyze heap dumps for
DirContextorNamingEnumerationobjects that are unexpectedly large or numerous, and trace their references toClassLoaders. - Fix: Always ensure that
DirContextobjects andNamingEnumerations are closed in afinallyblock. - Why it works: Closing these resources releases any held references, including potential references back to the
ClassLoaderthat performed the lookup.
- Diagnosis: Analyze heap dumps for
-
Reflection and Unsafe Operations: Using reflection to access or manipulate class metadata in ways that bypass normal GC mechanisms, or using
sun.misc.Unsafeto create class loaders or manipulate class instances, can lead toClassLoaders not being collected.- Diagnosis: This is harder to pinpoint without deep code inspection. Look for heavy use of reflection on classes and
ClassLoaders, and specificUnsafecalls. Heap dumps might showClassobjects with unusual lifecycles. - Fix: Re-evaluate the use of reflection and
Unsafe. If dynamic class loading is necessary, use standard APIs and ensure proper lifecycle management. - Why it works: Standard APIs are designed with GC in mind. Bypassing them can inadvertently create hard references that the GC cannot resolve.
- Diagnosis: This is harder to pinpoint without deep code inspection. Look for heavy use of reflection on classes and
-
Framework-Specific Issues: Certain frameworks, particularly older versions or those with complex lifecycle management (e.g., some older versions of Spring, OSGi implementations, or custom plugin architectures), can have built-in
ClassLoaderleak patterns.- Diagnosis: If the leak appears after introducing or upgrading a specific framework, consult the framework’s documentation and issue trackers. Heap dumps might reveal framework-internal objects holding onto
ClassLoaders. - Fix: Apply framework patches, upgrade to newer versions, or adjust configuration according to framework best practices for lifecycle management and resource cleanup.
- Why it works: Frameworks often manage class loading and component lifecycles. Fixing leaks within the framework ensures its internal mechanisms correctly release
ClassLoaderreferences.
- Diagnosis: If the leak appears after introducing or upgrading a specific framework, consult the framework’s documentation and issue trackers. Heap dumps might reveal framework-internal objects holding onto
After fixing these, the next error you’ll likely encounter is java.lang.OutOfMemoryError: Metaspace if you’ve been aggressively loading and unloading classes without addressing the underlying configuration or if the Metaspace itself is too small for the application’s legitimate class loading needs.