The #1 Fix for JVM Compressed Class Space Leaks (2025)
Tired of OutOfMemoryError: Compressed class space? Discover the #1 fix for JVM class space leaks in 2025, from diagnosis with JFR to permanent solutions.
Alexey Petrov
Senior Performance Engineer specializing in JVM optimization and large-scale system diagnostics.
Ever been greeted by the dreaded java.lang.OutOfMemoryError: Compressed class space
? It’s a subtle, frustrating error that can bring a production JVM to its knees. Unlike a typical heap space OOM, this one feels different, because it is. It’s a sign that your application's metadata, the very blueprint of its classes, is out of control.
In 2025, with modern frameworks and dynamic languages running on the JVM, this problem is more common than ever. But don't worry. There's a systematic way to tackle it, and we're going to walk through the #1 definitive fix—which, spoiler alert, is more of a strategy than a single magic flag.
What Exactly is the Compressed Class Space?
Before we can fix the leak, we need to understand the plumbing. In the world of 64-bit Java, every object pointer (a reference to a location in memory) would normally be 64 bits. This is inefficient. To save memory and improve performance, the JVM uses a brilliant trick called Compressed Ordinary Object Pointers (Compressed Oops).
This allows the JVM to use 32-bit pointers to address up to 32 GB of heap, which is a massive win. But this optimization requires that class metadata also live within a predictable, addressable range.
This is where Metaspace and the Compressed Class Space come in:
- Metaspace: Since Java 8, this is the area in native memory (outside the main heap) where the JVM stores class metadata. It can grow automatically by default.
- Compressed Class Space (CCS): This is a special, fixed-size region within Metaspace. The JVM uses it to store class metadata specifically to enable the use of compressed pointers. By default, its size is capped at 1 GB.
When the JVM can no longer fit new class metadata into this 1 GB reserved space, it throws the OutOfMemoryError: Compressed class space
. It hasn't run out of general memory, just this specific, optimized region.
The Root Cause: Why Does it *Really* Leak?
A "leak" in the Compressed Class Space isn't a memory leak in the traditional sense where memory is allocated and never freed. Instead, it’s a symptom of runaway class loading. Your application is generating and loading far more classes than the JVM can ever unload, eventually exhausting the fixed-size CCS.
Here are the most common culprits:
- Dynamic Code Generation: This is the biggest offender. Frameworks like Spring (for AOP proxies), Hibernate (for entity proxies), and libraries like ByteBuddy, CGLIB, and Javassist generate new classes at runtime. If these are not managed or cached properly, you can generate thousands of unique classes that never get unloaded.
- Leaky ClassLoaders: In environments with frequent redeployments, like application servers (Tomcat, WildFly) or OSGi frameworks, this is a classic problem. When an application is undeployed, its ClassLoader and all the classes it loaded should be garbage collected. However, if a single reference to that ClassLoader (or any object it loaded) is held by a part of the application that persists (like a thread pool or a logging context), the ClassLoader and all its classes are pinned in memory forever.
- Reflective and Lambda-driven Code: While less common as a sole cause, heavy, non-caching use of reflection or dynamically generated lambda functions can contribute. Every unique lambda expression can result in a new class being generated behind the scenes via
LambdaMetafactory
.
Diagnosing the Leak: Your Toolkit for 2025
Guessing is not a strategy. To fix the leak, you must first find its source. Modern JVMs provide an excellent set of tools for this job. You can start by enabling class unloading logging with -Xlog:class+unload=info
to see if classes are being unloaded at all during garbage collection.
Here’s a comparison of the best tools for the job:
Tool | Pros | Cons | Best For... |
---|---|---|---|
JFR + JMC | Low overhead, built-in to the JDK, great for production. Shows class loading/unloading trends over time. | Can be overwhelming. Doesn't always pinpoint the exact GCRoot holding a ClassLoader. | Getting a high-level overview and trend analysis in production environments without significant performance impact. |
jcmd <pid> GC.class_stats | Command-line, scriptable, precise. Gives a detailed breakdown of all loaded classes, their instance counts, and the memory they occupy, grouped by ClassLoader. | Snapshot-in-time view. Output can be verbose and requires some manual parsing or scripting. | Getting a definitive, point-in-time list of what's actually filling up the Compressed Class Space. This is often the most direct way to identify the problem classes. |
Heap Dumps (MAT, YourKit) | The most powerful tool for root cause analysis. Can trace GC Roots to find *exactly* why a ClassLoader isn't being collected. | Huge file sizes, high analysis overhead. Requires pausing the application, which may not be feasible in production. | The final, definitive step when other tools show a ClassLoader leak but you can't figure out what's holding onto it. |
VisualVM / JConsole | Free, easy to use, visual. Good for watching trends live and getting a quick feel for the problem. | Higher overhead than JFR. Less powerful for deep root cause analysis compared to heap dumps. | Quick, interactive monitoring in development or staging environments to confirm a leak is happening. |
The #1 Fix: A Systematic Approach to Resolution
The single most effective "fix" is not a JVM flag. It's a diagnostic and resolution process. Don't just treat the symptom; cure the disease.
The Band-Aid Fix (Don't Do This First!): You can increase the size of the CCS with
-XX:CompressedClassSpaceSize=2G
(or more). This is a temporary patch that buys you time. It doesn't fix the underlying leak and will only delay the inevitable crash. Use it to keep a system alive while you perform the real fix.
Here is the systematic approach that actually works:
Step 1: Identify the Leaking Classes with jcmd
This is your starting point. Connect to the running Java process and get a class histogram. The GC.class_stats
command is perfect for this because it links classes to their loaders and provides memory usage.
# First, get the PID of your Java process
jps -l
# Then, run GC.class_stats and pipe it to a file for analysis
jcmd <PID> GC.class_stats > class_stats.txt
Now, open class_stats.txt
and look for patterns. Are you seeing thousands of classes with names like:
com.myapp.MyService$$EnhancerBySpringCGLIB$$...
org.example.SomeClass$ByteBuddy$abC123
GeneratedMethodAccessor...
MyClass$$Lambda$123/0x...
If you see a huge number of similar-looking classes, especially with hexadecimal or numeric suffixes, you've found your culprit. Note the class names and, just as importantly, the memory address of their ClassLoader (it's listed in the output).
Step 2: Trace the ClassLoader GC Root with a Heap Dump
If you suspect a ClassLoader leak (e.g., you see multiple instances of your application's ClassLoader after a redeploy), it's time for a heap dump.
jmap -dump:format=b,file=heap.hprof <PID>
Load this heap.hprof
file into a tool like Eclipse MAT or YourKit Profiler.
- In MAT, run the "ClassLoader Leak Suspects" report. It's designed for this exact problem.
- If that's inconclusive, find an instance of one of your leaking classes identified in Step 1.
- Navigate from the class to its
ClassLoader
instance. - From the
ClassLoader
instance, find the path to its GC Root (in MAT, this is "Path to GC Roots" with the `exclude all phantom/weak/soft etc. references` option).
This will show you the exact chain of objects that is preventing your ClassLoader from being garbage collected. It's often a thread pool, a third-party library's cache, or a static field that's holding on to it.
Step 3: Implement the Real Fix in Your Code or Configuration
Once you know the *what* and the *why*, the fix becomes clear.
- Problem: CGLIB/ByteBuddy Proxy Spam.
Solution: Check your framework versions. Newer versions of Spring and Hibernate have better caching for proxies. In some cases, you might be able to refactor your code to use interface-based proxies (standard Java dynamic proxies) instead of class-based proxies (CGLIB), which can sometimes reduce the number of generated classes. - Problem: Leaky ClassLoader after Redeploy.
Solution: The heap dump will point you to the culprit. It's often a web application's responsibility to clean up its resources. Ensure your application's shutdown hooks (e.g., aServletContextListener
) properly terminate thread pools, deregister JDBC drivers, and clear any static caches. - Problem: Buggy Third-Party Library.
Solution: You've identified a library that's creating classes or holding onto ClassLoaders. The first step is to check for an updated version of that library—chances are, you're not the first person to hit this bug.
Beyond the Fix: Proactive Strategies for Prevention
Don't wait for the OOM to happen. A good engineer prevents fires, not just puts them out.
- Monitor Metaspace: Add Metaspace and Compressed Class Space usage to your primary monitoring dashboard (APM tools like DataDog, New Relic, or Prometheus JMX Exporter can all track this). Watch for steady, unexplained growth.
- Automate Leak Detection: Incorporate redeployment cycles into your performance and integration tests. Run a `GC.class_stats` check before and after to ensure the class count returns to its baseline.
- Enforce Architectural Rules: Use static analysis tools like ArchUnit to disallow reflection or dynamic class generation in parts of your application where it shouldn't be happening.
Final Takeaways
Fighting a Compressed Class Space leak can feel daunting, but it's entirely manageable with a systematic approach.
- A
Compressed class space
OOM is a metadata problem, not a heap problem, caused by runaway class loading. - The root cause is usually dynamic code generation (proxies), leaky ClassLoaders, or a combination of both.
- The #1 Fix is a process: Use
jcmd
to identify the problematic classes, use a heap dump to trace the GC root holding the ClassLoader, and then fix the underlying code, dependency, or configuration. - Increasing the space with
-XX:CompressedClassSpaceSize
is a temporary band-aid, not a cure. Use it only to buy time for a proper investigation.
By understanding the cause and using the right tools, you can move from reactive firefighting to proactive prevention, ensuring your JVMs run smoothly and efficiently.