Fix java.lang.OutOfMemoryError: Metaspace in Java Applications

intermediateโ˜• Java2026-04-08| Java 8+, JVM (HotSpot), Linux/macOS/Windows, Spring Boot, Tomcat, any long-running Java app

Error Message

java.lang.OutOfMemoryError: Metaspace
#java#jvm#metaspace#memory#classloader

The Error

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)

This one rarely shows up at startup. More often, the app runs fine for hours โ€” sometimes days โ€” then crashes. You might also find it buried inside a WrappedException from Hibernate or Spring, which makes it easy to miss.

What Metaspace Actually Is

Java 8 replaced PermGen with Metaspace. Metaspace stores class metadata โ€” the JVM's internal representation of every loaded class. Unlike heap, it lives in native memory. By default it's unbounded, but it can still exhaust available memory if your system is under pressure or if you've set -XX:MaxMetaspaceSize.

Two things usually cause this:

  • Metaspace cap too small โ€” you set -XX:MaxMetaspaceSize below what your app's class footprint actually needs.
  • Classloader leak โ€” classes keep loading but never unload. Dynamic proxies, scripting engines, and hot-reload code are the usual suspects.

Step 1 โ€” Check Your Current Metaspace Usage

Before touching any flags, get a baseline:

# While the app is running:
jcmd <PID> VM.native_memory summary

# Or via jstat (classes loaded):
jstat -class <PID> 1000 10

# Quick per-classloader snapshot:
jmap -clstats <PID> | sort -k3 -rn | head -20

In the jcmd output, find the Metaspace section. If committed is close to your MaxMetaspaceSize, you just need to raise the cap. But if jstat shows the class count climbing without stopping โ€” even during idle periods โ€” that's a leak, not a config problem.

Step 2 โ€” Raise the Metaspace Limit (Quick Fix)

A too-low -XX:MaxMetaspaceSize is the simplest case. Bump it:

# Add or adjust in your JVM args:
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

MetaspaceSize sets the initial threshold โ€” setting it higher avoids unnecessary GC on startup. MaxMetaspaceSize is the hard cap. For most Spring Boot apps, 256โ€“512m covers it. Apps with heavy plugin architectures or dynamic class generation (think Groovy-heavy or OSGi) may need 768m or more.

In application.properties (Spring Boot via Maven plugin):

spring-boot.run.jvmArguments=-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

In a Dockerfile:

ENV JAVA_OPTS="-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
CMD java $JAVA_OPTS -jar app.jar

In Tomcat's catalina.sh:

export JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"

Step 3 โ€” Diagnose a Classloader Leak

Raising the limit helped, but Metaspace is still growing? You have a leak. Heap dump analysis is the most reliable way to find it.

# Trigger a heap dump:
jmap -dump:format=b,file=heap.hprof <PID>

# Or configure the JVM to auto-dump on OOM:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/oom.hprof

Open the dump in Eclipse MAT or VisualVM. Go to the Class Loader Explorer. Seeing 500 instances of the same classloader for a single class? That's your leak right there.

Common culprits:

  • Libraries that spin up a new ClassLoader per request (some XML parsers, scripting engines)
  • Groovy/BeanShell/Velocity script caches with no size bound
  • Spring DevTools hot-reload running in production (don't ship it)
  • CGLIB or Javassist generating proxy classes inside a loop without caching
  • JDBC drivers registered on deploy but never deregistered on undeploy

Step 4 โ€” Fix the Leak

CGLIB / Dynamic Proxies

Each Enhancer.create() call produces a new class. Do it per-request and you'll flood Metaspace. Cache the proxy instead:

// BAD โ€” creates a new class on every call
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
MyService proxy = (MyService) enhancer.create();

// GOOD โ€” cache the proxy class
private static final MyService PROXY = createProxy();
private static MyService createProxy() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(MyService.class);
    return (MyService) enhancer.create();
}

Groovy Script Engine

A fresh GroovyShell per evaluation is a classic leak. Reuse one shell and cache compiled scripts:

// BAD
new GroovyShell().evaluate(script);

// BETTER โ€” reuse shell and cache compiled scripts
private final GroovyShell shell = new GroovyShell();
private final Map<String, Script> cache = new ConcurrentHashMap<>();

public Object eval(String src) {
    return cache.computeIfAbsent(src, shell::parse).run();
}

JDBC Driver on Tomcat Undeploy

Add cleanup logic to your webapp's ServletContextListener:

@Override
public void contextDestroyed(ServletContextEvent sce) {
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
        Driver driver = drivers.nextElement();
        try {
            DriverManager.deregisterDriver(driver);
        } catch (SQLException e) {
            log.warn("Failed to deregister driver", e);
        }
    }
}

Step 5 โ€” Verify the Fix

Monitor Metaspace over time after applying changes:

# Watch class count every 5 seconds for 2 minutes:
jstat -class <PID> 5000 24

# Or enable GC logging and grep for Metaspace:
-Xlog:gc*:file=/tmp/gc.log:time,uptime:filecount=5,filesize=20m
grep -i metaspace /tmp/gc.log | tail -20

A clean app plateaus after startup โ€” class count stabilizes and stays flat. Still seeing 100+ new classes per minute during normal operation? The leak isn't fixed yet.

For the limit-raise fix: run the app through several full request cycles and confirm Metaspace usage levels off well below your new cap. If it's still trending upward, you've got both problems.

Useful JVM Flags Summary

# Set initial + max Metaspace
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# Dump heap on OOM for post-mortem analysis
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/myapp/oom.hprof

# Enable native memory tracking (lightweight)
-XX:NativeMemoryTracking=summary

# Trigger GC more aggressively to reclaim class metadata
-XX:+CMSClassUnloadingEnabled   # Java 8 CMS only
-XX:+ClassUnloadingWithConcurrentMark  # G1/ZGC

Quick Checklist

  • Check if MaxMetaspaceSize is set too low โ€” raise it first
  • Monitor class count with jstat -class โ€” flat = good, still climbing = leak
  • Heap dump + MAT Class Loader Explorer to locate the leak source
  • Look for dynamic proxy or scripting engine misuse
  • Deregister JDBC drivers on webapp shutdown
  • Never run Spring DevTools in production

Related Error Notes