☕ 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

GC Algorithms
Garbage collection algorithms are the strategies the JVM uses to identify and reclaim unreachable objects. Different algorithms make different trade-offs between throughput, latency, and memory overhead. The foundational algorithms are mark-sweep, mark-compact, and copying — each with distinct properties that make them suitable for different parts of the heap and different application requirements. Modern production collectors (G1GC, ZGC, Shenandoah) combine these algorithms with concurrency, parallelism, and incremental collection to achieve the best overall performance. This entry covers the core algorithms in depth, their correctness properties, performance characteristics, why generational collection is effective, and how the major production collectors implement these algorithms.
Memory Leak
A memory leak in Java occurs when objects that are no longer needed by the application remain reachable through strong references, preventing the garbage collector from reclaiming their memory. Unlike C or C++, Java cannot leak memory through forgotten deallocation — the GC handles that. Java leaks memory through forgotten references: objects that the application logic no longer needs but that are still reachable through data structures, caches, listeners, or static fields. Memory leaks manifest as growing heap usage, increasing GC frequency and duration, and ultimately OutOfMemoryError. This entry covers every major leak pattern in Java, how to detect and diagnose leaks, how to fix each pattern, and tools for prevention.
finalize()
The finalize() method was a mechanism for performing cleanup on objects before they were garbage collected. It was deprecated in Java 9, deprecated for removal in Java 18, and removed entirely in Java 21. Understanding why finalize() was designed, why it was deprecated, what is wrong with it, and what replaces it is essential both for maintaining legacy code that uses it and for writing correct cleanup code going forward. This entry covers finalize() mechanics, its fundamental problems, why it was removed, and the correct modern alternatives: try-with-resources, Cleaner, and PhantomReference.
Reference Types (Weak, Soft, Phantom)
Java provides four reference strengths beyond the ordinary strong reference: strong (the default), soft, weak, and phantom. Each has a different policy for when the garbage collector is permitted to reclaim the referenced object. Soft references are collected when the JVM is low on memory — ideal for memory-sensitive caches. Weak references are collected at the next GC cycle — ideal for canonicalising maps and listeners. Phantom references are enqueued after the object is finalised but before its memory is reclaimed — ideal for post-collection cleanup actions. Understanding these reference types enables building caches that do not cause OutOfMemoryError, listeners that automatically deregister themselves, and deterministic cleanup of native resources. This entry covers all four strengths with their precise semantics, the ReferenceQueue mechanism, and the canonical use case for each type.