Introduction
Metaspace (introduced in Java 8) stores class metadata and replaces PermGen. Unlike the heap, Metaspace uses native memory. Frameworks like Spring, Hibernate, and Groovy generate classes at runtime using CGLIB, ByteBuddy, or ASM. When too many classes are generated or classloaders leak, Metaspace fills up and triggers OutOfMemoryError: Metaspace.
Symptoms
java.lang.OutOfMemoryError: Metaspacejava.lang.OutOfMemoryError: Compressed class space- Application crashes after running for hours/days
jstat -gc <pid>shows Metaspace (MU) approaching capacity (MC)- Hotspot:
java.lang.ClassLoaderinstances accumulating
Exception in thread "http-nio-8080-exec-15" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:135)
...Common Causes
- CGLIB/ByteBuddy generating a new proxy class per request
- Custom classloaders not being garbage collected
- Groovy/JSP dynamic compilation in production
- Hot reload in development leaking class definitions
MaxMetaspaceSizetoo small for the application
Step-by-Step Fix
- 1.Check Metaspace usage:
- 2.```bash
- 3.# Check current Metaspace usage
- 4.jstat -gc <pid> 1000
# Output: # S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU # 256.0 256.0 0.0 0.0 65536.0 32768.0 131072.0 65536.0 51200.0 49800.0 7680.0 7200.0 # MC = Metaspace Capacity, MU = Metaspace Used (nearing limit!)
# Enable Metaspace GC logging java -XX:+PrintGCDetails -XX:+PrintGCMetaData -jar app.jar ```
- 1.Increase Metaspace limits:
- 2.```bash
- 3.# Default is ~21MB, increase for production
- 4.java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -jar app.jar
# Compressed class space (for compressed oops) java -XX:CompressedClassSpaceSize=256m -jar app.jar ```
- 1.Fix CGLIB proxy class generation:
- 2.```java
- 3.// WRONG - creates a new class for each call
- 4.public UserService createProxy(UserService target) {
- 5.Enhancer enhancer = new Enhancer();
- 6.enhancer.setSuperclass(target.getClass());
- 7.enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
- 8.return method.invoke(target, args);
- 9.});
- 10.return (UserService) enhancer.create(); // New class every time!
- 11.}
// CORRECT - cache the Enhancer or use Spring's proxy mechanism // Spring caches CGLIB proxies by class, only generates once per class @EnableAspectJAutoProxy(proxyTargetClass = true) ```
- 1.Detect classloader leaks:
- 2.```bash
- 3.# Take a heap dump
- 4.jmap -dump:format=b,file=heapdump.hprof <pid>
# Analyze with Eclipse MAT or VisualVM # Look for: # - org.springframework.cglib.core.KeyFactory$$Key # - java.lang.ClassLoader with many loaded classes # - groovy.lang.GroovyClassLoader instances
# Use jcmd for class histogram jcmd <pid> GC.class_histogram | head -30 ```
- 1.Configure Groovy class generation limits:
- 2.```java
- 3.// Groovy generates a class per script by default
- 4.// Use GroovyClassLoader with caching
- 5.import groovy.lang.GroovyClassLoader
- 6.import groovy.lang.GroovyCodeSource
class ScriptCache { private final Map<String, Class> cache = new ConcurrentHashMap<>() private final GroovyClassLoader loader = new GroovyClassLoader()
Class compile(String name, String script) { cache.computeIfAbsent(name, { loader.parseClass(script, name) // Class is cached by name }) } } ```
Prevention
- Set
-XX:MaxMetaspaceSize=512m(or appropriate for your app) in production - Monitor Metaspace usage with JMX:
java.lang:type=MemoryPool,name=Metaspace - Enable
-XX:+CMSClassUnloadingEnabled(for CMS GC) or rely on G1's default class unloading - Use
-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=trueto debug proxy generation - Add Metaspace to Grafana/Prometheus monitoring dashboards
- Set up alerts when Metaspace usage exceeds 80% of MaxMetaspaceSize
- In Kubernetes, set memory requests accounting for Metaspace:
-Xmx+ Metaspace + overhead