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:

  1. 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.Class objects and investigate which ClassLoader instances are holding onto them. Specifically, search for ThreadLocalMap entries that point to ClassLoader instances.

    • Command (MAT): File -> Open Heap Dump, then Dominator Tree or Path to GC Roots with exclude weak/soft references.
    • Fix: Identify the ThreadLocal variable responsible. Ensure that the ThreadLocal’s remove() method is called in a finally block 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 ThreadLocal reference breaks the chain of references from the thread to the ClassLoader, allowing the ClassLoader and its loaded classes to be garbage collected.
  2. Custom ClassLoader Stays Alive: If your application dynamically loads classes using custom ClassLoaders (e.g., for plugin systems, hot-reloading), these ClassLoaders 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 ClassLoader and trace their Path to GC Roots. Pay close attention to static fields.
    • Fix: Ensure that custom ClassLoader instances 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 ClassLoader makes it eligible for garbage collection, which in turn allows the classes it loaded to be collected.
  3. 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 a ThreadLocal as mentioned above), and the server reuses the thread for another deployment or task, the leaked ClassLoader from the previous context can persist.

    • Diagnosis: Use jstack to examine thread dumps. Look for threads in the server’s pool that are holding onto ClassLoader instances, especially if you see multiple instances of the same class loaded by different ClassLoaders.
    • 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 dereference ClassLoaders 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.
  4. JNDI Context Leak: Applications using JNDI (Java Naming and Directory Interface) can sometimes leak ClassLoaders. If a ClassLoader is used to look up JNDI resources and the NamingEnumeration or DirContext is not properly closed, it might hold a reference to the ClassLoader.

    • Diagnosis: Analyze heap dumps for DirContext or NamingEnumeration objects that are unexpectedly large or numerous, and trace their references to ClassLoaders.
    • Fix: Always ensure that DirContext objects and NamingEnumerations are closed in a finally block.
    • Why it works: Closing these resources releases any held references, including potential references back to the ClassLoader that performed the lookup.
  5. Reflection and Unsafe Operations: Using reflection to access or manipulate class metadata in ways that bypass normal GC mechanisms, or using sun.misc.Unsafe to create class loaders or manipulate class instances, can lead to ClassLoaders not being collected.

    • Diagnosis: This is harder to pinpoint without deep code inspection. Look for heavy use of reflection on classes and ClassLoaders, and specific Unsafe calls. Heap dumps might show Class objects 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.
  6. 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 ClassLoader leak 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 ClassLoader references.

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.

Want structured learning?

Take the full Jvm course →