Have you ever faced this? The heap looks fine. CPU is stable. Traffic hasn’t changed. And then suddenly, your application crashes with OutOfMemoryError: Metaspace. If you want a broader understanding of different OutOfMemoryError scenarios and how they occur, you can explore our guide on types of OutOfMemoryError in Java.
This is the classic symptom of a ClassLoader memory leak, a problem that standard heap monitoring cannot detect because Metaspace lives outside the heap in native memory. It commonly affects Java EE applications and microservices running on servers like Tomcat, JBoss, or WebSphere, especially when dynamic class generation or repeated redeployments are involved.
You can throw all the -Xmx you want at this problem but it won’t help. The real fix requires identifying and resolving the underlying Class Loader memory leak.
This article walks you through how to diagnose, fix, and prevent these leaks in production.
Understanding the Metaspace and ClassLoader Relationship
To understand how ClassLoader memory leaks occur, it helps to think about a simple analogy. Imagine a large production system as the Titanic sailing across the ocean. In this analogy, the ship represents the JVM, and the passengers represent the classes loaded by the application.
Inside the ship are many cabins, each holding groups of passengers. These cabin sections represent ClassLoaders. The space that stores all the structural information about these passengers (classes) is Metaspace. As more passengers board the ship, this storage area gradually fills up.
We all know what happened to the Titanic, it struck an iceberg and eventually sank. In our scenario, the iceberg represents a ClassLoader memory leak that disrupts the production system. A single unresolved reference, such as a lingering ThreadLocal, a static cache entry, or an improperly deregistered resource, can prevent a ClassLoader from being released.
Because memory is reclaimed at the ClassLoader level, the JVM cannot free classes individually. If a ClassLoader remains alive, every class it loaded continues to occupy space in Metaspace. Over time, as new deployments occur and new ClassLoaders are created, the old ones begin to accumulate.
Just like the Titanic slowly filled with water after the collision, Metaspace continues to fill as these unused ClassLoaders remain in memory. Eventually, the JVM runs out of Metaspace and crashes with OutOfMemoryError: Metaspace.
Understanding this relationship between Metaspace, ClassLoaders, and memory reclamation is key to diagnosing and fixing ClassLoader memory leaks in production environments.
The Three Conditions for Class Unloading
The JVM will only get rid of a class and free up its Metaspace when three things are true at the time:
- No live instances of the class exist anywhere
- The ClassLoader that loaded the class is not pointing to it anymore
- No other active classes loaded by the ClassLoader are still being used
The JVM and the ClassLoader have this third condition that is really tough. Even if 99 out of 100 classes loaded by a ClassLoader are completely gone one surviving reference to one class keeps the ClassLoader alive and all 100 classes stay in Metaspace.
This means that to properly fix a ClassLoader memory leak we need to ask one question: what is still holding a reference, to the ClassLoader?
Symptoms of ClassLoader Memory Leaks and Metaspace OutOfMemoryError
Do you know why it is hard to diagnose ClassLoader memory leaks? It is because the system may appear stable and the heap usage might look healthy, CPU utilization may remain normal, and application traffic may not change significantly. Meanwhile, Metaspace continues to grow silently in the background until the JVM eventually crashes.
Like the Titanic slowly taking on water after the collision, ClassLoader leaks often build up quietly before the system finally fails. Below are some of the most common warning signs that indicate a possible ClassLoader memory leak:
- OutOfMemoryError: Metaspace with healthy heap: A crash with OutOfMemoryError: Metaspace while heap usage remains low usually points to a ClassLoader-related memory leak.
- Frequent Full GC at low heap usage: Repeated Full GC cycles despite low heap occupancy often indicate the JVM is trying to unload classes to reclaim Metaspace.
- Metaspace steadily increasing: Continuous Metaspace growth, especially after redeployments, suggests that old ClassLoaders are not being released.
- Increasing ClassLoader count: A rising number of ClassLoader instances in heap dumps typically signals ClassLoader accumulation.
- Performance degradation after redeployments: Gradual slowdown after multiple deployments can occur when unused ClassLoaders remain in memory.
- Native memory growth with stable heap: If overall JVM memory keeps increasing while heap stays stable, Metaspace usage in native memory may be expanding.
If you are starting to notice these warning signs in production, the next step is to confirm whether ClassLoaders are actually accumulating in memory, which means you need to start digging into GC logs, heap dumps, and ClassLoader statistics.
Diagnosing ClassLoader Memory Leaks
Let’s be honest. When your system throws an OutOfMemoryError: Metaspace, our first instinct is often to increase heap size or tune GC settings. And most of the time, that doesn’t solve the problem too, because it had nothing to do with heap at all.
It’s similar to inspecting the upper decks of the Titanic while the real damage was happening below the waterline. Heap metrics may look perfectly healthy, while the actual problem is building up quietly in Metaspace.
ClassLoader leaks live in Metaspace, which is part of the JVM’s native memory. That means the usual heap graphs and dashboards may not reveal the real problem. Instead, diagnosing a ClassLoader leak requires examining JVM artifacts such as GC logs, heap dumps, and ClassLoader statistics.
Fixing ClassLoader Memory Leaks
Now that we’ve identified the reference that is keeping a ClassLoader alive, the solution is pretty simple: Fixing it. Below are some of the most common fixes used in real-world production systems:
- Clean up ThreadLocal variables: Thread pools reuse threads, so ThreadLocal values can remain long after a request completes. Always call remove() after use to prevent the ClassLoader from being retained.
- Deregister JDBC drivers during shutdown: JDBC drivers registered with DriverManager may persist across redeployments if not explicitly deregistered. Clean them up during application shutdown.
- Shut down executor services: Threads created by the application may continue running after the application stops. Always shut down thread pools so they do not retain references to application classes.
- Avoid static references to application objects: Static caches, registries, or global objects can keep references to classes loaded by the application ClassLoader. Clear these references during shutdown or use weak references.
- Upgrade problematic libraries: Some ClassLoader leaks originate from third-party libraries. Check whether the issue has been fixed in a newer version before implementing complex workarounds.
- Limit dynamic class generation: Frameworks such as CGLIB, ByteBuddy, ASM, or Groovy can generate classes dynamically. Ensure generated classes are cached and reused instead of being created repeatedly. For a deeper look at how dynamic class loading can cause ClassLoader leaks, see our article “The Dreaded ClassLoader Leak: When Dynamic Code Loading Goes Wrong.”
Fixing a ClassLoader memory leak resolves the immediate issue, but preventing it from happening again requires adopting a few important engineering practices.
Preventing ClassLoader Memory Leaks in Production
We’ve already established that Classloader Memory Leaks are difficult to diagnose, but it is far more easier to prevent. You know what they say: Prevention is better than cure. Just as better safety practices could have reduced the damage during the Titanic disaster, proper resource management and monitoring can prevent ClassLoader leaks from silently growing in production systems.
Below are some important preventive measures:
- Clean up resources such as thread pools, JDBC drivers, and registries during application shutdown.
- Always remove ThreadLocal variables after request or task completion.
- Avoid long-lived static references to application objects or caches.
- Monitor Metaspace usage and ClassLoader counts, not just heap memory.
- Enable heap dumps using -XX:+HeapDumpOnOutOfMemoryError for easier troubleshooting.
- Reuse dynamically generated classes or proxies instead of generating them repeatedly.
Tools to Diagnose ClassLoader Memory Leaks
Diagnosing ClassLoader memory leaks often requires analyzing multiple JVM artifacts such as GC logs, heap dumps, thread dumps, and native memory statistics. The following tools can help identify the root cause more efficiently.
- GCeasy: Analyzes GC logs and helps identify abnormal Metaspace growth and repeated Full GC events caused by class unloading attempts.
- HeapHero: Analyzes heap dumps to detect ClassLoader accumulation, inspect class histograms, and trace reference chains that prevent ClassLoaders from being garbage collected.
- yCrash: Captures JVM artifacts such as heap dumps, thread dumps, and GC logs during a crash, helping engineers quickly identify memory-related issues.
- fastThread: Analyzes thread dumps and helps detect ThreadLocal-related leaks that may keep ClassLoaders alive.
- JDK Native Memory Tracking (NMT): Provides insights into native memory usage, including Metaspace consumption, helping identify memory growth outside the heap.
- Eclipse MAT (Memory Analyzer Tool): Allows deep inspection of heap dumps and helps trace object reference chains to identify what is keeping a ClassLoader alive.
Real-World Case Study
A microservice on an internal platform was crashing repeatedly with OutOfMemoryError: Metaspace. Interestingly, the heap was sitting at just 10% usage when Full GC events started firing. The team had already increased -Xmx twice, but the problem persisted.
Using yCrash, the team captured a heap dump, thread dump, and GC log at the moment of failure. The heap dump, analyzed in HeapHero, revealed that the number of ClassLoader instances had grown into the hundreds, far beyond what a single microservice should normally have. The class histogram revealed the next clue: thousands of auto-generated class definitions with UUID-style names, all pointing to the same third-party library.
Tracing the reference chain confirmed the root cause. The library was generating a new class definition for every incoming HTTP request without caching or reusing them. The issue had already been fixed in a newer version of the library, but the application was still running an older vulnerable version.
The fix turned out to be surprisingly simple: a dependency version upgrade. After the update, the ClassLoader count stabilized immediately, Metaspace growth stopped, and the repeated Full GC events disappeared.
The iceberg had been there the whole time, the heap dashboard simply wasn’t showing it.
For a detailed walkthrough of the investigation process, see the original analysis: Troubleshooting Microservices OutOfMemoryError: Metaspace.
Conclusion
ClassLoader memory leaks doesn’t usually make any fuzz, and that is why we should consider them to be dangerous. As stated earlier in the blog, staring from the heap to CPU utilization to application traffic, everything looks normal and yet, poof… OutOfMemoryError: Metaspace happens. The pattern and the reason often remains the same: Metaspace fills silently below the surface, Full GC cycles begin firing as the JVM attempts to unload classes, and eventually the system runs out of native memory because ClassLoaders were never released.
The solution is not simply increasing heap size or tuning GC settings. A proper ClassLoader memory leak fix requires identifying the reference that is keeping the ClassLoader alive, whether it’s a forgotten ThreadLocal, a static cache, or an improperly deregistered resource. It also means ensuring proper cleanup during application shutdown and monitoring Metaspace usage, not just heap memory.
If your application is crashing with OutOfMemoryError: Metaspace while heap usage looks normal, don’t guess. Capture a heap dump, analyze the ClassLoader count, and let the data reveal the real cause.

Share your Thoughts!