skip to content

What is 'OutOfMemoryError: Metaspace', what kinds of problems produce it, and how does it differ from a heap-space OOME?

level: seniorimportance: should knowfreq 55%

answer

  1. Metaspace = class metadata, native memory, not the heap
  2. Tuned by -XX:MaxMetaspaceSize, NOT -Xmx
  3. Java 8 replaced PermGen with Metaspace
  4. ClassLoader leak: a loader can't unload while referenced → pinned classes
  5. Climbs with hot redeploys; find path-to-GC-root of the stuck loader

basics

~20 s

Metaspace is the area that stores class metadata (the definitions of loaded classes), separate from the object heap. This OOME means too many classes are loaded — often because classes keep being created (dynamic proxies, code generation) or because old classes can't be unloaded (a classloader leak). Heap-space OOME is about objects; Metaspace is about class definitions.

solid answer

~50 s

Metaspace is the native-memory region (since Java 8, replacing PermGen) that holds **class metadata** — the loaded definition of each class, methods, constant pools, annotations. 'OutOfMemoryError: Metaspace' means that region is exhausted. Two cause families: (1) **legitimately too many classes** — heavy use of frameworks/proxies/bytecode generation, lots of dynamically generated classes, or just a `-XX:MaxMetaspaceSize` set too low; (2) a **classloader leak** — a ClassLoader (and therefore all the classes it loaded) can't be unloaded because something still references it or one of its classes/instances/static fields. This is classic in app servers and hot-redeploy: each redeploy spins up a new ClassLoader, the old one is pinned by a stray reference, and Metaspace climbs until it blows after N redeploys. It differs from heap-space OOME fundamentally: heap holds *objects* (instances), Metaspace holds *class definitions*; raising `-Xmx` does nothing for Metaspace (you tune `-XX:MaxMetaspaceSize`), and you diagnose it by watching loaded-class counts and hunting which ClassLoader can't be collected — not just object retained sizes.

code

java · 12 lines
java
// A classloader-leak trigger on hot redeploy: a ThreadLocal set on a
// POOLED thread holds a value whose class was loaded by the app's
// ClassLoader. The pooled thread outlives the app, so the value (and
// thus the whole ClassLoader and all its classes) can't be reclaimed.
public class Context {
    private static final ThreadLocal<Context> CURRENT = new ThreadLocal<>();

    public static void enter() { CURRENT.set(new Context()); }
    // BUG: no matching CURRENT.remove() before the thread returns to the pool.
    // Across redeploys, each old app's ClassLoader stays pinned in Metaspace,
    // eventually -> OutOfMemoryError: Metaspace.
}

go deeper

for a junior

Knows Metaspace holds class information (not objects) and that this OOME relates to too many classes being loaded.

for a middle

Separates Metaspace from the heap, knows it's tuned by MaxMetaspaceSize (not -Xmx) and replaced PermGen in Java 8, and can name dynamic-class-generation as a cause.

for a senior

Explains the classloader-leak mechanism (a loader pinned by a reference can't be unloaded), recognizes the hot-redeploy signature, and diagnoses via loaded-class counts and path-to-GC-root on the stuck loader.

for a principal

Sets platform practices for redeployable systems: ThreadLocal/driver/JMX cleanup on undeploy, MaxMetaspaceSize policy, class-loading observability (JFR/jstat), and architectural choices (e.g. immutable deploys/restart-over-redeploy) that sidestep loader leaks.

## Heap vs. Metaspace The heap stores **instances** — the objects you make with `new`. **Metaspace** stores **class metadata**: for every class the JVM loads, it keeps the class's structure — its methods' bytecode, field layout, the constant pool, annotations, and a link to the `ClassLoader` that loaded it. Crucially, Metaspace lives in **native memory** (outside the Java heap), and before Java 8 this lived in a fixed heap region called **PermGen**; Java 8 replaced PermGen with Metaspace, which by default grows dynamically but can be capped with **`-XX:MaxMetaspaceSize`**. ## What 'OutOfMemoryError: Metaspace' means It's thrown when the JVM needs to load (or generate) another class but Metaspace can't grow enough — either it hit `MaxMetaspaceSize` or the native memory limit. The number of distinct **loaded classes** is the thing that grew, not the number of objects. ## Cause family 1: legitimately too many classes Some workloads load or generate a *lot* of classes: - Frameworks that synthesize classes at runtime (dynamic proxies via `java.lang.reflect.Proxy`, CGLIB/ByteBuddy, ORM enhancement, expression/template compilers). - Heavy reflection or scripting engines compiling scripts into classes. - Simply running a large app with a small `-XX:MaxMetaspaceSize`. Here the loaded-class count is high but **stable** once warm — it just doesn't fit. Fix: raise `MaxMetaspaceSize` (or remove an over-tight cap). ## Cause family 2: classloader leak (the dangerous one) Class-unloading rule: **a `ClassLoader` can be unloaded (freeing all the classes it loaded from Metaspace) only when the loader itself, all the classes it defined, and all instances of those classes become unreachable.** A single dangling reference pins the whole loader. Why this bites in practice — **hot redeploy / app servers**: 1. App is deployed inside a fresh `WebAppClassLoader`. 2. You redeploy: the server creates a *new* loader for the new version and discards the old app. 3. But something outside the app still references the old loader or one of its classes — e.g. a `ThreadLocal` whose value's class was loaded by the old loader, a JDBC driver registered in a shared `DriverManager`, a static cache in a shared library keyed by the old class, a shutdown hook, a running thread from a pool. 4. The old loader can't be unloaded; its classes stay in Metaspace. 5. Repeat across redeploys → loaded-class count and Metaspace climb → `OutOfMemoryError: Metaspace` after N redeploys. Signature: Metaspace / loaded-class count grows specifically around redeploys (or around whatever creates new loaders), and you can find **multiple instances of the 'same' class** loaded by different ClassLoaders. ## How it differs from heap-space OOME | | Java heap space | Metaspace | |---|---|---| | Stores | object instances | class metadata (definitions) | | Location | Java heap | native memory | | Tuned by | `-Xmx` | `-XX:MaxMetaspaceSize` | | Grows because | too many/too-large reachable objects | too many loaded classes / unfreeable ClassLoaders | | Diagnose with | heap dump, retained size, dominator tree | loaded-class count over time, find the pinned ClassLoader (path-to-GC-root of the loader) | Raising `-Xmx` does **nothing** for a Metaspace OOME — a common mistake. ## Diagnosing and fixing - Monitor **loaded vs. unloaded class counts** (`jstat -class`, JFR, `-verbose:class`). Steadily rising loaded count with little unloading hints at a loader leak. - Take a heap dump and, in MAT, find **ClassLoader** objects and run *path to GC root* on the one that should have died — that reference is your leak. Or use MAT's duplicate-class detection. - Fix the pin: remove `ThreadLocal`s in pooled threads on undeploy, deregister JDBC drivers and JMX beans, stop spawned threads, clear static caches that key on app classes. - If it's genuinely just many classes (no leak), raise `MaxMetaspaceSize` and/or reduce dynamic class generation. ## Key takeaways - Metaspace = class metadata in native memory; capped by `MaxMetaspaceSize`, not `-Xmx`. - Two causes: too many classes (stable, just resize) or a **classloader leak** (climbs with redeploys, a loader can't be unloaded because something references it). - Diagnosis is loaded-class-count + path-to-GC-root on the stuck ClassLoader, not object retained sizes. - Bumping `-Xmx` is the classic wrong move here.

  • Why does raising -Xmx not help a Metaspace OOME?
    Because Metaspace is a separate region (native memory) holding class metadata, not the object heap. -Xmx sizes the heap; you size Metaspace with -XX:MaxMetaspaceSize. They're independent.
  • What changed between PermGen and Metaspace, and why does it matter for this error?
    Java 8 removed PermGen (a fixed-size heap region for class metadata) and replaced it with Metaspace in native memory that grows by default. So instead of 'PermGen space' OOME at a hard cap, you now get 'Metaspace' OOME — and if you don't set MaxMetaspaceSize, a leak can consume native memory until the OS limit.

If the heap is a warehouse of products (object instances), Metaspace is the filing cabinet of blueprints (class definitions). You can run out of blueprint drawers even with an empty warehouse — and a classloader leak is like never being allowed to shred the old blueprints because one clerk still has a sticky note pointing at them.

saying these in an interview costs you the question

  • Trying to fix a Metaspace OOME by increasing -Xmx (wrong region entirely).
  • Thinking Metaspace stores objects — it stores class definitions/metadata.
  • Saying Metaspace is part of the Java heap — it's native memory.
  • Forgetting that a ClassLoader can't be unloaded while it (or any of its classes/instances/static fields) is still reachable.
  • Still calling it PermGen on Java 8+ as if nothing changed.

context