Introduction

Java OutOfMemoryError: Metaspace occurs when the JVM runs out of memory for storing class metadata. Unlike heap space which stores objects, metaspace stores class definitions, method metadata, constant pools, and JIT information. This error is common in applications that dynamically generate classes (Spring proxies, Groovy scripts, JasperReports), deploy/undeploy applications frequently, or have class loader leaks. When metaspace is exhausted, the JVM cannot load new classes, causing application failures that often require restart to recover.

Symptoms

  • Application logs show java.lang.OutOfMemoryError: Metaspace
  • Error includes Metaspace or Compressed class space in message
  • Application slows before crash as class loading fails
  • Issue appears after multiple deployments, hot reloads, or dynamic script execution
  • Works after restart but fails again after hours/days of operation
  • jstat -gc shows M (metaspace) column at or near maximum
  • Error occurs during new feature use that triggers new class loading

Common Causes

  • Class loader leak: Old class loaders retained, preventing GC of loaded classes
  • Excessive dynamic class generation (CGLIB proxies, Groovy scripts, JasperReports)
  • Framework scanning loading duplicate classes (OSGi, modular applications)
  • Insufficient MaxMetaspaceSize for application requirements
  • Memory leak in script engine (JavaScript, Groovy) compiling new classes
  • Application server hot deploy/undeploy without proper cleanup
  • Third-party libraries generating many proxy classes

Step-by-Step Fix

### 1. Enable metaspace diagnostic logging

Configure JVM to track class loading:

```bash # Add to JVM startup options -XX:+PrintClassHistogram -XX:+PrintMetaspaceStatistics -XX:MetaspaceReclaimThreshold=1 -XX:NativeMemoryTracking=detail

# For Java 8 -XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=256m

# For Java 11+ -XX:MaxMetaspaceSize=512m -XX:InitialMetaspaceSize=256m

# Enable class loading debug -XX:+TraceClassLoading -XX:+TraceClassUnloading

# Log class loader information -Xlog:class+load=info:classloading.log -Xlog:class+unload=info:classunloading.log ```

Monitor metaspace with jstat:

```bash # Monitor metaspace usage in real-time jstat -gc <pid> 1000

# Output columns: # S0/S1/Eden/Old (heap) # M0/M1/MC/CCS (metaspace) # YGC/YGCT/FGC/FGCT/GCT (GC stats)

# Key columns: # MC = Metaspace capacity (KB) # CCSC = Compressed class space capacity (KB) # YGC = Young GC count # FGC = Full GC count

# Log to file for analysis jstat -gc <pid> 1000 > /tmp/gc-stats.log &

# Watch metaspace growth over time watch -n 5 'jstat -gc <pid> | tail -1' ```

### 2. Capture heap dump with class loader info

Analyze class loader retention:

```bash # Capture heap dump jcmd <pid> GC.heap_dump -all /tmp/heap.hprof

# The -all flag includes all classes, critical for metaspace analysis

# Or with jmap jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>

# For Kubernetes pods kubectl exec <pod-name> -- jcmd 1 GC.heap_dump -all /tmp/metaspace.hprof kubectl cp <pod-name>:/tmp/metaspace.hprof ./metaspace.hprof ```

Analyze with Eclipse MAT:

```bash # Open heap dump in MAT

# Run Class Loader Analysis # Window > Preference > Memory Analysis > Class Loader Analysis # Right-click heap dump > Class Loader Analysis

# Key queries in MAT:

# 1. Find class loader leaks SELECT * FROM java.lang.ClassLoader

# 2. Find classes loaded by specific loader SELECT * FROM java.lang.Class WHERE classLoader = <leaking-loader-id>

# 3. Find GC roots retaining class loaders # Right-click ClassLoader > Path to GC Roots > Exclude all phantom/weak references

# 4. Find duplicate classes SELECT c.name, COUNT(*) as count FROM java.lang.Class c GROUP BY c.name HAVING COUNT(*) > 1 ORDER BY count DESC ```

MAT OQL queries for metaspace analysis:

```sql -- Show class loader hierarchy SELECT toString(cl) AS loader, toString(cl.getParent()) AS parent FROM java.lang.ClassLoader cl

-- Find web app class loaders (common leak source) SELECT * FROM org.apache.catalina.loader.WebappClassLoader

-- Find Groovy class leaks SELECT * FROM groovy.lang.GroovyClassLoader

-- Find leaked proxy classes SELECT * FROM java.lang.Class WHERE toString(name).contains('$Proxy') OR toString(name).contains('$$EnhancerBy')

-- Count classes per class loader SELECT toString(classLoader), COUNT(*) FROM java.lang.Class GROUP BY classLoader ORDER BY COUNT(*) DESC ```

### 3. Identify class loader leak patterns

Common class loader leak scenarios:

```java // Pattern 1: Static reference to class-loaded object public class LeakyStatic { // Holds reference to class loaded by webapp classloader private static final Object cache = new ExpensiveObject(); }

// When undeploying, WebappClassLoader cannot be GC'd // because static field holds reference

// Fix: Clear static references in shutdown hook @PreDestroy public void cleanup() { LeakyStatic.cache = null; }

// Pattern 2: ThreadLocal not removed (Tomcat common leak) public class LeakyThreadLocal { private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); }

// ThreadLocal value holds class reference // Thread lives beyond application lifecycle // ClassLoader cannot be GC'd

// Fix: Remove ThreadLocal in shutdown @PreDestroy public void cleanup() { formatter.remove(); }

// Pattern 3: JDBC driver not deregistered // Driver loaded by webapp classloader // DriverManager (in bootstrap classpath) holds reference

// Fix: Deregister in shutdown @PreDestroy public void cleanup() { Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); try { DriverManager.deregisterDriver(driver); log.info("Deregistered JDBC driver: " + driver); } catch (SQLException e) { log.warn("Failed to deregister: " + driver, e); } } }

// Pattern 4: Background thread not stopped public class LeakyThread { public void start() { new Thread(() -> { while (true) { // Holds reference to this class process(); } }).start(); // Non-daemon thread prevents GC } }

// Fix: Use daemon thread and stop on shutdown private volatile boolean running = true;

public void start() { Thread thread = new Thread(() -> { while (running) { process(); } }); thread.setDaemon(true); // Daemon threads don't prevent GC thread.start(); }

@PreDestroy public void stop() { running = false; } ```

### 4. Detect dynamic class generation leaks

Find excessive proxy/script generation:

```java // Monitor class loading at runtime import java.lang.management.ManagementFactory; import java.lang.management.ClassLoadingMXBean;

public class ClassLoadMonitor { private final ClassLoadingMXBean classLoadingMXBean; private long lastLoadedCount = 0;

public ClassLoadMonitor() { this.classLoadingMXBean = ManagementFactory.getClassLoadingMXBean(); }

public void checkClassLoading() { long loadedCount = classLoadingMXBean.getTotalLoadedClassCount(); long newClasses = loadedCount - lastLoadedCount;

if (newClasses > 100) { // Alert if > 100 classes loaded since last check log.warn("{} new classes loaded since last check", newClasses);

// Dump class loading info Thread.dumpStack(); // See what triggered loading }

lastLoadedCount = loadedCount; } }

// Detect CGLIB proxy explosion @Configuration public class ProxyConfig {

@Bean public static BeanPostProxyDetector proxyDetector() { return new BeanPostProxyDetector(); }

static class BeanPostProxyDetector implements BeanPostProcessor { private final Map<String, Integer> proxyCounts = new ConcurrentHashMap<>();

@Override public Object postProcessAfterInitialization(Object bean, String beanName) { String className = bean.getClass().getName(); if (className.contains("$$EnhancerBy")) { proxyCounts.merge(beanName, 1, Integer::sum);

int count = proxyCounts.get(beanName); if (count > 10) { log.warn("Bean {} has {} proxy classes - possible leak", beanName, count); } } return bean; } } }

// Detect Groovy class leak @Component public class GroovyScriptRunner {

private final GroovyClassLoader loader = new GroovyClassLoader(); private final Map<String, Class<?>> compiledScripts = new ConcurrentHashMap<>();

public Object runScript(String scriptName, String script) { // WRONG: Creates new class every time // Class<?> scriptClass = loader.parseClass(script);

// CORRECT: Cache compiled scripts Class<?> scriptClass = compiledScripts.computeIfAbsent(scriptName, k -> { return loader.parseClass(script); });

// Limit cache size if (compiledScripts.size() > 1000) { // Remove oldest entries compiledScripts.entrySet().stream() .sorted(Comparator.comparing(Entry::getKey)) .limit(compiledScripts.size() - 500) .forEach(e -> compiledScripts.remove(e.getKey())); } } } ```

### 5. Tune metaspace configuration

Set appropriate metaspace limits:

```bash # Production server tuning (Spring Boot application) # For typical enterprise application with moderate dynamic class usage

# Java 8 -XX:MetaspaceSize=256m # Initial metaspace threshold -XX:MaxMetaspaceSize=512m # Maximum metaspace -XX:CompressedClassSpaceSize=256m # Compressed class space

# Java 11+ -XX:InitialMetaspaceSize=256m -XX:MaxMetaspaceSize=512m

# For applications with heavy dynamic class generation # (Spring with extensive proxying, Groovy scripts, JasperReports)

-XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m -XX:CompressedClassSpaceSize=512m

# For microservices with minimal dynamic classes -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m

# Warning: Setting MaxMetaspaceSize too high can cause # native memory exhaustion. Monitor with NativeMemoryTracking. ```

Class unloading tuning:

```bash # Enable class unloading (enabled by default with G1) -XX:+UnlockExperimentalVMOptions -XX:+ClassUnloading

# For G1 GC -XX:+UseG1GC -XX:G1PeriodicGCInterval=5000 # Periodic GC every 5 seconds -XX:G1PeriodicGCSystemLoadThreshold=70 # Only if system load < 70%

# Force class unloading during Full GC -XX:+CMSClassUnloadingEnabled # For CMS GC (Java 8)

# Reduce metaspace reclaim threshold (more aggressive reclaim) -XX:MetaspaceReclaimThreshold=1 ```

### 6. Configure framework class loading

Spring Framework optimization:

```java // Spring Boot: Disable development features in production

// application-prod.yml spring: devtools: restart: enabled: false # Prevents class loader leaks livereload: enabled: false thymeleaf: cache: true # Enable template caching jackson: serialization: write-dates-as-timestamps: false

// Disable Spring Bean overriding (can cause duplicate class loading) spring.main.allow-bean-definition-overriding=false

// Limit component scanning @SpringBootApplication(scanBasePackages = {"com.example.mypackage"}) public class Application { }

// Avoid @ComponentScan with broad base packages // Instead of: @ComponentScan("com") // Too broad!

// Use: @ComponentScan("com.example.specific") ```

Hibernate/JPA class loading:

```java // Hibernate: Enable bytecode enhancement carefully // application.yml spring: jpa: properties: hibernate: bytecode: use_reflection_optimizer: false # Disable in production

# Or disable entirely if causing issues # hibernate.use_new_generator_mappings=false

// For applications with many entities, limit metadata processing spring.jpa.hibernate.ddl-auto=validate # Don't generate schema at runtime ```

### 7. Fix script engine class leaks

Proper script engine management:

```java // JavaScript (Nashorn) leak fix public class SafeScriptEngine {

// WRONG: Creates new engine each time public Object evaluateWrong(String script) throws ScriptException { ScriptEngine engine = new ScriptEngineManager() .getEngineByName("JavaScript"); return engine.eval(script); // New classes every call! }

// CORRECT: Reuse engine with proper cleanup private final ScriptEngine engine;

public SafeScriptEngine() { ScriptEngineManager manager = new ScriptEngineManager(); this.engine = manager.getEngineByName("JavaScript");

// Configure engine for safety Bindings bindings = engine.createBindings(); bindings.put("java.lang.Class", null); // Prevent Java access engine.setBindings(bindings, ScriptContext.GLOBAL_SCOPE); }

public Object evaluate(String script) throws ScriptException { synchronized (engine) { // Nashorn is not thread-safe return engine.eval(script); } }

@PreDestroy public void cleanup() { if (engine instanceof Closeable) { try { ((Closeable) engine).close(); } catch (IOException e) { log.warn("Failed to close engine", e); } } } }

// Groovy script caching public class CachedGroovyRunner {

private final GroovyClassLoader loader; private final Cache<String, Script> scriptCache;

public CachedGroovyRunner() { this.loader = new GroovyClassLoader( Thread.currentThread().getContextClassLoader() );

this.scriptCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(1, TimeUnit.HOURS) .build(key -> { // Compile script on cache miss Class<?> scriptClass = loader.parseClass(key); return InvokerHelper.createScript(scriptClass); }); }

public Object run(String scriptName, String scriptContent) { Script script = scriptCache.get(scriptContent); return script.run(); }

@PreDestroy public void cleanup() { scriptCache.invalidateAll(); loader.clearCache(); // Clear compiled classes loader.close(); // Allow GC } } ```

### 8. Monitor metaspace in production

Add monitoring for early detection:

```java // Spring Boot Actuator metrics import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean; import java.util.List;

@Component public class MetaspaceMetrics {

private final List<MemoryPoolMXBean> memoryPools;

public MetaspaceMetrics() { this.memoryPools = ManagementFactory.getMemoryPoolMXBeans(); }

@Scheduled(fixedRate = 60000) // Check every minute public void checkMetaspace() { for (MemoryPoolMXBean pool : memoryPools) { if ("Metaspace".equals(pool.getName())) { MemoryUsage usage = pool.getUsage(); long used = usage.getUsed(); long max = usage.getMax();

double percentUsed = (double) used / max * 100;

log.info("Metaspace: {}MB / {}MB ({:.1f}%)", used / 1024 / 1024, max / 1024 / 1024, percentUsed);

if (percentUsed > 80) { log.warn("Metaspace usage above 80% - consider increasing MaxMetaspaceSize"); }

if (percentUsed > 95) { log.error("Metaspace usage critical (>95%) - OOM risk"); } } } } }

// Prometheus metrics configuration @Configuration public class MetricsConfig {

@Bean public MeterBinder metaspaceBinder() { return (registry) -> { List<MemoryPoolMXBean> memoryPools = ManagementFactory.getMemoryPoolMXBeans();

for (MemoryPoolMXBean pool : memoryPools) { if ("Metaspace".equals(pool.getName())) { MemoryUsage usage = pool.getUsage();

Gauge.builder("jvm.metaspace.used", usage, u -> u.getUsed()) .description("Metaspace used in bytes") .baseUnit("bytes") .register(registry);

Gauge.builder("jvm.metaspace.max", usage, u -> u.getMax()) .description("Metaspace max in bytes") .baseUnit("bytes") .register(registry); } } }; } } ```

Prometheus alerting:

```yaml groups: - name: java_metaspace rules: - alert: JavaMetaspaceHigh expr: jvm_memory_used_bytes{pool="Metaspace"} / jvm_memory_max_bytes{pool="Metaspace"} > 0.8 for: 10m labels: severity: warning annotations: summary: "Java Metaspace usage above 80%" description: "Metaspace usage is {{ $value | humanizePercentage }}"

  • alert: JavaMetaspaceCritical
  • expr: jvm_memory_used_bytes{pool="Metaspace"} / jvm_memory_max_bytes{pool="Metaspace"} > 0.95
  • for: 5m
  • labels:
  • severity: critical
  • annotations:
  • summary: "Java Metaspace usage critical"
  • description: "Metaspace at {{ $value | humanizePercentage }} - OOM imminent"
  • alert: JavaMetaspaceGrowing
  • expr: deriv(jvm_memory_used_bytes{pool="Metaspace"}[1h]) > 104857600
  • for: 1h
  • labels:
  • severity: warning
  • annotations:
  • summary: "Java Metaspace growing continuously"
  • description: "Metaspace growing at more than 100MB/hour - possible class leak"
  • `

### 9. Application server specific fixes

Tomcat class loader leak prevention:

```xml <!-- context.xml - Add to META-INF --> <Context> <!-- Clear ThreadLocal values on undeploy --> <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>

<!-- Stop threads on undeploy --> <Listener className="com.example.CustomLeakPreventionListener"/> </Context>

<!-- Custom leak prevention listener --> public class CustomLeakPreventionListener implements ServletContextListener {

@Override public void contextDestroyed(ServletContextEvent sce) { // Stop all daemon threads started by this webapp ThreadGroup tg = Thread.currentThread().getThreadGroup(); Thread[] threads = new Thread[tg.activeCount()]; tg.enumerate(threads);

for (Thread thread : threads) { if (thread.getContextClassLoader() .getClass().getName() .contains("WebappClassLoader")) { log.info("Stopping thread: " + thread.getName()); thread.interrupt(); } }

// Clear JDBC drivers Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); if (driver.getClass().getClassLoader() == sce.getServletContext().getClassLoader()) { try { DriverManager.deregisterDriver(driver); } catch (SQLException e) { log.warn("Failed to deregister", e); } } }

// Clear ThreadLocals // (Use reflection to access ThreadLocal internals) } } ```

### 10. Implement graceful degradation

Handle metaspace pressure gracefully:

```java @Component public class MetaspaceAwareHandler {

private static final double CRITICAL_THRESHOLD = 0.95; private final List<MemoryPoolMXBean> memoryPools;

public MetaspaceAwareHandler() { this.memoryPools = ManagementFactory.getMemoryPoolMXBeans(); }

public boolean canLoadMoreClasses() { for (MemoryPoolMXBean pool : memoryPools) { if ("Metaspace".equals(pool.getName())) { MemoryUsage usage = pool.getUsage(); double usageRatio = (double) usage.getUsed() / usage.getMax(); return usageRatio < CRITICAL_THRESHOLD; } } return true; }

public <T> Class<?> loadClassSafely(String className) { if (!canLoadMoreClasses()) { throw new RuntimeException( "Cannot load class " + className + " - metaspace pressure"); }

try { return Class.forName(className); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }

// Use in dynamic class loading scenarios public Object createProxySafely(Object target) { if (!canLoadMoreClasses()) { // Return original object instead of creating proxy log.warn("Skipping proxy creation due to metaspace pressure"); return target; }

// Create proxy as normal return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> method.invoke(target, args) ); } } ```

Prevention

  • Set MaxMetaspaceSize appropriate for application type
  • Monitor metaspace usage with Prometheus/Grafana
  • Clear ThreadLocals in shutdown hooks
  • Deregister JDBC drivers on undeploy
  • Stop all threads started by application
  • Cache dynamically compiled scripts
  • Use daemon threads for background operations
  • Review class loading patterns in code review
  • **OutOfMemoryError: Compressed class space**: 64-bit JVM class space exhausted
  • **OutOfMemoryError: unable to create new native thread**: Thread creation failed
  • **ClassNotFoundException**: Class cannot be loaded
  • **NoClassDefFoundError**: Class was present at compile but missing at runtime