Metaspace explained: OutOfMemoryError: Metaspace, class loader leaks and sizing

Every class the JVM loads needs a description: its methods, bytecode, constant pool, field layout. Since JDK 8 that metadata lives in Metaspace, a native memory area outside the Java heap. It is usually quiet, until an application loads classes faster than it unloads them.
From PermGen to Metaspace
Before JDK 8, class metadata lived in the permanent generation, a fixed-size part of the heap, and the familiar error was OutOfMemoryError: PermGen space. JEP 122 removed the permanent generation in JDK 8 and moved class metadata to native memory. The PermGen flags (-XX:PermSize, -XX:MaxPermSize) no longer apply.
In JDK 16, JEP 387 (Elastic Metaspace) reworked the allocator so that unused metaspace memory is returned to the operating system more promptly and per-class-loader overhead is reduced.
What is in it, and what is not
- In Metaspace: class and method metadata, bytecode, constant pools, annotations, and related structures for each loaded class.
- Not in Metaspace:
staticfield values and thejava.lang.Classobjects themselves, which are in the heap; JIT-compiled code, which is in the code cache; interned strings, which are in the heap.
Metadata is allocated per class loader and freed only when the class loader itself becomes unreachable and is collected. A class cannot be unloaded on its own while its loader is alive. This is the root of every Metaspace leak.
The flags
| Option | Effect |
|---|---|
-XX:MaxMetaspaceSize | Upper bound for class metadata. The java reference states that by default the size is not limited. |
-XX:MetaspaceSize | The threshold at which reaching it first triggers a garbage collection to unload classes; the threshold is then adjusted. The default depends on the platform. |
-XX:CompressedClassSpaceSize | With compressed class pointers (the default on 64-bit), class structures go into a separate, fixed-size reserved range called the compressed class space; this option sizes it. |
"Not limited" does not mean free: an unbounded Metaspace grows until the process or container runs out of memory, and the failure then looks like an OS kill rather than a clear Java error. Setting -XX:MaxMetaspaceSize to a generous but finite value turns a leak into an explicit OutOfMemoryError: Metaspace, which is far easier to diagnose.
The two error messages
java.lang.OutOfMemoryError: Metaspace: total class metadata hitMaxMetaspaceSize(or native memory ran out).java.lang.OutOfMemoryError: Compressed class space: the compressed class space is full even though Metaspace overall may not be. RaiseCompressedClassSpaceSizeif the number of classes is legitimately large; otherwise look for a leak exactly as below.
The GC log shows Metaspace pressure before any error, as collections with the cause Metadata GC Threshold. A growing number of those over time is an early warning.
Monitoring
jstat
$ jstat -gc <pid> 10s
The MC and MU columns are Metaspace capacity and usage; CCSC and CCSU are the compressed class space. See the jstat reference for each column.
jcmd VM.metaspace
The most detailed view. The jcmd reference documents options including basic, show-loaders, show-classes, by-spacetype and scale:
$ jcmd <pid> VM.metaspace
$ jcmd <pid> VM.metaspace show-loaders scale=MB
show-loaders lists usage per class loader, which is exactly what you need to spot a loader that should not exist any more, or hundreds of copies of the same loader type.
Class loader views
$ jcmd <pid> VM.classloader_stats
$ jcmd <pid> VM.classloaders
The first prints, per loader, the number of classes and the metadata size; the second shows the loader hierarchy. Compare two outputs taken some time apart.
Class loading and unloading logs
-Xlog:class+load=info,class+unload=info:file=/var/log/app/classes.log
A class that is loaded again and again, with generated names that differ each time, points straight at the code that generates it.
Common causes of growth
Redeploying into a long-running server
Application servers load each deployment in its own class loader. If anything outside the application keeps a reference to any object from the old deployment, the old loader and all its classes stay. Typical culprits: a JDBC driver registered with DriverManager by the application and never deregistered, threads started by the application and never stopped, ThreadLocal values left in server threads, and shutdown hooks. Each redeploy then adds a full copy of the application's metadata.
Generated classes
Proxies, bytecode-generation libraries, expression languages, scripting engines and serialisation frameworks can generate classes at runtime. If they generate a new class per call instead of caching it (for example, compiling a script or template on each request), Metaspace grows with traffic.
Simply many classes
A very large application may need more metadata than a tight limit allows. If usage plateaus after warm-up, the limit is just too low.
Finding the leaking loader
- Confirm growth with
jstat -gcorVM.metaspaceover time. - Use
VM.metaspace show-loadersorVM.classloader_statsto identify the loader type that multiplies. - Take a heap dump and look for instances of that class loader type. In Eclipse Memory Analyzer, the class loader explorer and duplicate classes views show loaders with the same classes.
- Run Path to GC Roots on an old loader instance, excluding weak references. The path ends at whatever holds it: the fix is to release that reference at undeploy. See GC roots and memory leaks and reading a heap dump.
Sizing in a memory budget
Metaspace counts against a container's memory limit like any other native memory. Measure steady-state usage after warm-up (VM.metaspace, or the Class category of Native Memory Tracking), set MaxMetaspaceSize comfortably above it, and include that figure in the total budget described in the JVM in containers.
Sources
- JEP 122: Remove the Permanent Generation and JEP 387: Elastic Metaspace
- The java command (MaxMetaspaceSize, MetaspaceSize, CompressedClassSpaceSize)
- The jcmd command (VM.metaspace, VM.classloader_stats, VM.classloaders) and the jstat command
MemoryGC roots and memory leaks
What counts as a GC root, the usual accidental references, and how to walk from a suspect object back to its root.
MemoryNative Memory Tracking
Heap is only part of the process: read the NMT summary, diff against a baseline, and know what it cannot see.
MemoryThe code cache and ReservedCodeCacheSize
What happens when compiled code runs out of room, the 240 MB default, segmented heaps and how to monitor them.