☕ Java

Metaspace

Metaspace is the JVM memory region that stores class metadata — the internal representations of loaded classes, methods, fields, constant pools, and annotations. It replaced PermGen (Permanent Generation) in Java 8. Unlike PermGen which was a fixed-size heap region, Metaspace is allocated from native memory (outside the Java heap) and grows dynamically up to an optional maximum. Understanding Metaspace means understanding what class metadata contains, what causes Metaspace to grow, how class unloading reclaims Metaspace, what OutOfMemoryError from Metaspace looks like, how to monitor and limit it, and what the practical implications are for application servers, OSGi containers, and frameworks that generate classes dynamically.

What Metaspace Contains and Why It Exists

Metaspace stores the internal data structures that the JVM needs to describe loaded classes. For each loaded class this includes: the runtime representation of the class itself (InstanceKlass in HotSpot), the method bytecode (or compiled native code for JIT-compiled methods), the constant pool of the class (strings, class references, method references resolved to internal pointers), field descriptors (names, types, offsets), method descriptors and exception tables, annotations, and vtables for virtual method dispatch. The key insight is that this metadata is per class per classloader. If the same class (same binary name, same bytes) is loaded by two different ClassLoader instances, two separate metadata entries exist in Metaspace — one for each ClassLoader. This is how Java application servers achieve class isolation between deployed applications: each deployment gets its own ClassLoader, so its classes are independent instances with independent metadata. Metaspace replaced PermGen in Java 8 for a practical reason: PermGen had a fixed maximum size (-XX:MaxPermSize, default 64-256MB depending on JVM version) that was notoriously easy to exhaust on application servers deploying and redeploying applications. When PermGen filled, the JVM threw OutOfMemoryError: PermGen space and often became unstable. By moving to native memory, Metaspace can grow as large as the available native memory allows, eliminating the most common source of PermGen errors. The tradeoff is that unbounded Metaspace can consume all available native memory, which is why configuring -XX:MaxMetaspaceSize is still recommended in production.
Java
// ── What triggers Metaspace growth ───────────────────────────────────

// 1. Loading new classes — each class consumes Metaspace
ClassLoader cl = new URLClassLoader(new URL[]{jarUrl});
Class<?> clazz = cl.loadClass("com.example.MyClass");
// MyClass's metadata now occupies Metaspace

// 2. JIT compilation — compiled methods stored in Metaspace (CodeCache)
// As methods get hot and are JIT-compiled, CodeCache grows
// CodeCache is adjacent to but distinct from Metaspace

// 3. Dynamic class generation — frameworks that generate proxy classes
// These frameworks add classes to Metaspace:
// - Spring AOP (CGLIB proxies, JDK proxies)
// - Hibernate (proxy entities for lazy loading)
// - Jackson (serialiser/deserialiser classes)
// - Java Reflection (Method, Constructor access classes)
// - Mockito (mock subclasses)
// - Bytecode manipulation (ASM, ByteBuddy, Javassist)

// 4. Class reloading in dev mode (Spring DevTools, JRebel)
// New versions of classes are loaded while old versions wait for GC
// Both old and new consume Metaspace until old classloaders are GC'd

// ── Metaspace layout (HotSpot) ────────────────────────────────────────
// Class space:       holds per-class InstanceKlass structures
//                    (with compressed class pointers, typically 1GB limit)
// Non-class space:   all other metadata (methods, constants, etc.)
//
// JVM flags:
// -XX:MetaspaceSize=<size>       initial Metaspace size (triggers GC at this threshold)
// -XX:MaxMetaspaceSize=<size>    maximum (default: unlimited — set this in production!)
// -XX:CompressedClassSpaceSize=<size>  limit for class space (default 1GB)

// ── Monitoring Metaspace ──────────────────────────────────────────────
// jcmd <pid> VM.metaspace
// jstat -gc <pid>    (displays MC: Metaspace Capacity, MU: Metaspace Used)
// jmap -clstats <pid>  (class loader statistics)

// GC log entry showing Metaspace change:
// [gc] Pause Full (Metadata GC Threshold) 512M->180M(1024M) 345.ms
//                 ↑                                         ↑
//   Metaspace threshold triggered the GC        full heap GC occurred

Class Unloading and Metaspace Reclamation

Metaspace is reclaimed when a ClassLoader becomes unreachable and is garbage collected. The JVM cannot unload individual classes — it can only unload all classes loaded by an entire ClassLoader at once, as an atomic operation. When a ClassLoader and all classes it has loaded become unreachable (no live instances of those classes, no accessible references to the ClassLoader or the Class objects), the entire set of metadata for those classes is freed. This all-or-nothing unloading requirement means that application servers can leak Metaspace if any object of a class from a ClassLoader survives after the ClassLoader should have been collected. A single static reference to an object of a hot-deployed class keeps the entire ClassLoader — and all thousands of classes it loaded — alive in Metaspace. This is the root cause of the "PermGen leak" (or in Java 8+, the "Metaspace leak") pattern: repeated redeployments of an application eventually exhaust Metaspace because old ClassLoaders accumulate. Bootstrap classes (java.lang.*, java.util.*, etc.) are loaded by the bootstrap ClassLoader and are never unloaded. The extension ClassLoader and application ClassLoader are typically also never unloaded in standalone applications. Class unloading matters primarily for application servers, OSGi containers, and plugin systems that create and discard ClassLoaders dynamically. JVM flags that affect class unloading: -XX:+ClassUnloading (enabled by default) allows the JVM to unload classes. -XX:+CMSClassUnloadingEnabled was needed for old CMS collector but is no longer relevant for G1GC and newer collectors which support class unloading by default.
Java
// ── When Metaspace IS reclaimed (class unloading) ────────────────────
// Condition: ClassLoader becomes unreachable

URLClassLoader loader = new URLClassLoader(urls);
Class<?> clazz = loader.loadClass("com.example.Plugin");
Object plugin = clazz.getDeclaredConstructor().newInstance();

// Make everything unreachable:
plugin = null;   // no live instances of Plugin
clazz  = null;   // no live Class<Plugin> references
loader = null;   // ClassLoader is now unreachable

System.gc();     // encourage (not guarantee) GC

// After GC: loader collected → Plugin metadata freed from Metaspace

// ── Metaspace leak — ClassLoader retained by accident ─────────────────
// LEAK: static field holds reference to an instance of a hot-deployed class
public class AppServer {

    // This static field in the SERVER's ClassLoader holds an instance
    // of Plugin which was loaded by the PLUGIN's ClassLoader
    private static Object leakedPlugin;   // <-- LEAK

    public void deploy(URLClassLoader pluginLoader) throws Exception {
        Class<?> clazz = pluginLoader.loadClass("com.example.Plugin");
        leakedPlugin = clazz.getDeclaredConstructor().newInstance();
        // pluginLoader can NEVER be collected while leakedPlugin is alive
        // All Plugin metadata stays in Metaspace FOREVER
    }
}

// Fixed: use weak reference or clear on undeploy
private static WeakReference<Object> safePlugin;

public void undeploy() {
    safePlugin = null;   // or leakedPlugin = null
    // Now the pluginLoader CAN be collected
}

// ── Diagnosing Metaspace leaks ────────────────────────────────────────
// 1. Watch Metaspace growth over redeployments:
//    jstat -gc <pid> 5000  (every 5 seconds)
//    Look for MC (Metaspace capacity) growing after each undeploy

// 2. Find retained ClassLoaders with heap dump:
//    jmap -dump:format=b,file=heap.hprof <pid>
//    Open in Eclipse MAT → OQL:
//    SELECT * FROM java.lang.ClassLoader

// 3. Identify ClassLoader retention path:
//    MAT → Find path from GC roots to each ClassLoader
//    The path shows what is holding the ClassLoader alive

// 4. jcmd output shows classloader stats:
//    jcmd <pid> VM.classloader_stats

Metaspace Tuning and OutOfMemoryError

OutOfMemoryError: Metaspace occurs when Metaspace cannot grow further — either because -XX:MaxMetaspaceSize has been reached or because the OS cannot provide more native memory. Unlike heap OutOfMemoryError, which typically indicates a memory leak or undersized heap, Metaspace OutOfMemoryError often indicates either too many unique classes being loaded (normal for large frameworks) or a ClassLoader leak (abnormal, a bug). The first step in diagnosing Metaspace OutOfMemoryError is to determine whether Metaspace is growing unboundedly (leak) or whether it has simply stabilised at a value larger than MaxMetaspaceSize (size misconfiguration). If Metaspace grows continuously over time even without new class loading activity, a ClassLoader leak is likely. If Metaspace grows quickly at startup and then stabilises, the application simply needs more Metaspace and MaxMetaspaceSize should be increased. Setting -XX:MaxMetaspaceSize in production is important for two reasons: it prevents Metaspace from consuming all available native memory (which would starve other processes or cause the OS to swap), and it causes OutOfMemoryError with a useful message rather than an OS-level kill. However, setting it too low causes premature OutOfMemoryError. A reasonable approach is to measure Metaspace usage at peak load, add a 50-100% safety margin, and set MaxMetaspaceSize to that value. -XX:MetaspaceSize (note: no "Max") sets the initial size at which the JVM first performs a Metaspace GC. Setting this higher delays the first GC trigger but does not limit growth.
Java
// ── Recommended Metaspace JVM flags ──────────────────────────────────
// Development (allow growth, no limit):
// (no MaxMetaspaceSize — default unlimited)

// Production (bounded, monitored):
// java -XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=128m MyApp

// Container/microservice (small footprint):
// java -XX:MaxMetaspaceSize=256m MyApp

// Application server with plugins (larger):
// java -XX:MaxMetaspaceSize=1g MyApp

// ── Diagnosing Metaspace OutOfMemoryError ─────────────────────────────
// java -XX:+HeapDumpOnOutOfMemoryError
//      -XX:HeapDumpPath=/dumps/
//      -XX:MaxMetaspaceSize=512m
//      -Xlog:class+load=info    ← log every class load
//      -Xlog:gc*                ← log GC including Metaspace
//      MyApp

// -Xlog:class+load output:
// [0.342s] Loaded com.example.UserService from file:/app/app.jar
// [0.343s] Loaded com.example.UserService$$SpringProxy$abc from null
// [1.234s] Loaded com.example.UserService from file:/app/app.jar  ← second load!
// ↑ Same class loaded twice = two ClassLoaders (possible leak)

// ── Monitoring Metaspace in production ───────────────────────────────
// Via JMX / Prometheus JMX exporter:
// java.lang:type=MemoryPool,name=Metaspace
//   .Usage.used      — current Metaspace bytes used
//   .Usage.committed — current allocated (may be higher than used)
//   .Usage.max       — MaxMetaspaceSize (-1 if unlimited)

// Key alert thresholds:
// Metaspace.used > 80% of MaxMetaspaceSize → WARNING
// Metaspace growth rate > 0 after warm-up → investigate ClassLoader leak

// ── Framework-specific Metaspace considerations ────────────────────────
// Spring Boot:
//   Each @Configuration class, @Bean, proxy generates 1-3+ Metaspace entries
//   Typical Spring app: 100-300MB Metaspace at startup
//   spring-boot-devtools: reloads classes → watch for accumulation

// CGLIB / ByteBuddy (Spring AOP, Mockito):
//   Each new proxy class = new entry in Metaspace
//   Mockito in tests: creates proxy per mock() call
//   Use @ExtendWith(MockitoExtension.class) to manage lifecycle

// Groovy / JRuby / dynamic languages embedded in JVM:
//   Scripts may generate a new class per eval — very high Metaspace growth
//   Reuse compiled scripts; avoid per-request compilation

Related Topics in Java Memory Management

Stack Memory
Stack memory is the region of memory where the JVM stores method invocation frames, local variables, and partial results. Every thread has its own private stack created at thread creation, and the stack grows and shrinks as methods are called and return. Stack memory operates on a last-in, first-out discipline — the frame for the most recently called method sits on top, and when that method returns its frame is immediately discarded. Understanding stack memory explains why local variables are thread-safe by default, why recursive algorithms can cause StackOverflowError, why primitive values behave differently from objects, and what the JVM does at every method call and return. This entry covers stack frame structure, the stack pointer, local variable storage, operand stacks, frame lifecycle, thread isolation, and the performance characteristics that make stack allocation extremely fast.
Heap Memory
The heap is the runtime data area from which all Java object instances and arrays are allocated. It is shared across all threads in a JVM process, grows dynamically up to a configured maximum, and is managed entirely by the garbage collector. Understanding heap memory means understanding how objects are allocated, how the generational hypothesis drives GC design, how the major garbage collectors (G1, ZGC, Shenandoah, Parallel) partition and manage the heap, what triggers garbage collection, how to tune heap size and GC behaviour, and how to diagnose heap-related problems including OutOfMemoryError and excessive GC pause times. This entry covers heap structure, generational design, object allocation, garbage collection triggers, common collectors, heap tuning, and memory leak detection.
JVM Memory Model
The JVM Memory Model encompasses two related but distinct concepts. The first is the runtime memory architecture — how the JVM partitions memory into regions (stack, heap, Metaspace, code cache, native memory) and what each region stores. The second, more precise meaning is the Java Memory Model (JMM) defined in the Java Language Specification — the formal set of rules that govern how multithreaded programs observe memory: when writes made by one thread become visible to other threads, what ordering guarantees exist, and how volatile, synchronized, and final provide those guarantees. This entry covers both: the complete runtime memory architecture with all regions explained, and the Java Memory Model's happens-before relationship, visibility rules, and the practical consequences for concurrent code.
Garbage Collection
Garbage collection is the automatic process by which the JVM reclaims memory occupied by objects that are no longer reachable from the running program. It is one of Java's most defining features — eliminating the manual memory management required in C and C++ and the entire class of bugs that comes with it: use-after-free, double-free, and memory leaks caused by forgotten deallocation. Understanding garbage collection means understanding reachability, GC roots, the collection process, what the GC guarantees and what it does not, how to work with it rather than against it, and how to diagnose and resolve GC-related performance problems. This entry covers the reachability model, GC triggers, what GC does not collect, finalization, the GC performance trade-off triangle, and practical guidance for writing GC-friendly code.