Java applications rely on the Java Virtual Machine (JVM) for memory management. Beyond the heap, the JVM must also store class metadata, the structural information required to load, link, and execute classes at runtime.
As the title suggests, this article explores how class metadata storage evolved in modern Java, specifically, Metaspace vs. PermGen. To understand this transition, we’ll take a quick trip down memory lane. Let’s get started.
JVM Memory Regions Refresher (pre-Java 8)
I meant it when I said we need to take a trip down memory lane, but what I really meant was the Java Memory and its regions. JVM has the following memory regions:

Fig: JVM Memory Regions
- Young Generation: Newly created application objects are stored in this region.
- Old Generation: Application objects that are living for longer are promoted from the Young Generation to the Old Generation. Basically, this region holds long-lived objects.
- PermGen (Permanent Generation): Stored class definitions, method metadata, and other structures required for class loading and execution. It was a fixed-size region within the JVM heap (prior to Java 8).
- Threads: Each application thread requires a thread stack. Space allocated for thread stacks, which contain method call information and local variables are stored in this region.
- Code Cache: Memory areas where compiled native code (machine code) of methods is stored for efficient execution are stored in this region.
- Direct Buffer: ByteBuffer objects are used by modern framework (i.e. Spring WebClient) for efficient I/O operations. They are stored in this region.
- GC (Garbage Collection): Memory required for automatic garbage collection to work is stored in this region.
- JNI (Java Native Interface): Memory for interacting with native libraries and code written in other languages are stored in this region.
- Misc: There are areas specific to certain JVM implementations or configurations, such as the internal JVM structures or reserved memory spaces. They are classified as ‘misc’ regions.
Did you notice something missing? Metaspace.
And that is because, prior to Java 8, Metaspace did not exist. Class metadata was stored exclusively in PermGen. Due to its fixed sizing limitations and frequent memory errors, PermGen was removed and replaced by Metaspace starting in Java 8.
Note: If you’d like a deeper understanding of JVM memory regions and the different types of memory failures associated with them, you can explore our detailed guide on Types of OutOfMemoryError.
What is PermGen?
As discussed in the earlier section, PermGen was a dedicated memory region in the JVM heap used to store class metadata prior to Java 8. Whenever the JVM loaded a class, it stored structural information in PermGen, including the Class constant pool and Internal JVM class-related structures. Because PermGen was part of the heap, it was managed by the garbage collector. However, unlike other heap regions, its size was fixed by default and had to be explicitly tuned using JVM parameters such as:
-XX:PermSize-XX:MaxPermSize

Fig: ‘java.lang.OutOfMemoryError: Permgen space’
Applications that dynamically generated or loaded large numbers of classes, such as those using reflection, proxies, or bytecode manipulation libraries, consumed PermGen space rapidly.
Why PermGen Was Removed
Although it played a crucial role, PermGen suffered from a number of architectural and operational constraints, which limited its performance in contemporary Java workloads.
Fixed Memory Limits
PermGen had a hard upper bound. If class metadata consumption exceeded this limit, the JVM would throw java.lang.OutOfMemoryError: PermGen space. This was commonly observed in application servers (Tomcat, WebLogic, etc.), hot redeploy environments, and applications that relied heavily on dynamic class generation.
Difficult Capacity Planning
Sizing PermGen appropriately was challenging. Allocating too little memory led to frequent OutOfMemoryErrors, while allocating too much reduced the heap available for application objects. Since metadata growth depends on runtime class loading behavior, static sizing often proved inefficient.
Classloader Memory Leaks
PermGen was highly susceptible to classloader leaks. In environments with frequent application redeployments, classloaders were not always garbage collected properly. As a result, class metadata accumulated over time, eventually exhausting PermGen space even when heap usage appeared normal.
Heap Coupling Constraints
Because PermGen was part of the JVM heap, it competed directly with object allocation memory. Increasing PermGen reduced the space available for Young and Old Generation objects, making memory tuning a trade-off between application data and metadata storage.
Operational Tuning Overhead
Managing PermGen required explicit JVM tuning using parameters like -XX:PermSize and -XX:MaxPermSize. This added operational complexity, particularly in large-scale or containerized deployments where workloads varied.
What is Metaspace?
Metaspace is the memory region introduced in Java 8 to store class metadata, replacing the earlier Permanent Generation (PermGen). When the JVM loads classes, it needs space to hold structural information required for execution. In Metaspace, this includes:
- Class definitions and hierarchy metadata
- Method bytecode and method metadata
- Runtime constant pool
- Static variables (class-level)
- Internal classloader structures

Fig: ‘java.lang.OutOfMemoryError: Metaspace’
Metaspace does not exist inside the JVM heap as PermGen does. Rather, it is assigned from native memory (off-heap). This architectural decision eliminated the fixed-size invariant that had led to frequent metadata-related memory failures in the past. Metaspace can grow dynamically in response to the classes used in that application by default. Still, it is limited by the native memory available on the host system at large. When this space is depleted, the JVM throws: java.lang.OutOfMemoryError: Metaspace
To control its growth, JVM tuning parameters such as the following can be used:
-XX:MetaspaceSize-XX:MaxMetaspaceSize
This new design significantly improved flexibility for modern Java applications that rely heavily on dynamic class generation, reflection, and modular classloading mechanisms.
Architecture Differences
We’ve looked at PermGen and Metaspace separately. Now, let’s take a closer look at what the architecture difference are:
| Architecture Aspect | PermGen | Metaspace |
| Memory Location | Resided within the JVM heap | Resides in native (off-heap) memory |
| Allocation Model | Fixed-size memory region | Dynamically expands by default |
| Default Size Behavior | Predefined and limited | System-memory bound unless capped |
| Tuning Requirement | Mandatory manual tuning | Optional tuning |
| JVM Parameters | -XX:PermSize, -XX:MaxPermSize | -XX:MetaspaceSize, -XX:MaxMetaspaceSize |
| Garbage Collection Coupling | Managed as part of heap GC | Reclaimed via class unloading from native memory |
| Classloader Memory Handling | Shared fixed pool for all classloaders | Uses per-classloader Metachunks |
| OOM Failure Trigger | Fixed space exhaustion | Native memory exhaustion or cap breach |
| Redeploy / Dynamic Classload Impact | High risk of memory leaks and OOM | More resilient to dynamic classloading |
| Introduced / Removed | Removed in Java 8 | Introduced in Java 8 |
This architectural redesign eliminated fixed metadata sizing constraints and laid the foundation for more scalable classloading in modern Java runtimes.
Monitoring & Diagnosing Metaspace Issues
With Metaspace storing class metadata in native memory, heap-only monitoring is no longer enough. To avoid runtime failures, engineers must monitor classloading activity, native memory usage, and metadata growth.
Key Metrics to Monitor
When diagnosing Metaspace health, the following indicators are critical:
- Metaspace utilization vs. configured maximum
- Class loading and unloading rates
- Active classloader count
- Native memory consumption trends
- Full GC frequency driven by metadata pressure
A sustained increase in these indicators often points to classloader leaks, excessive dynamic class generation, or framework lifecycle mismanagement.
Note: For deeper investigative workflows, including retained heap analysis, classloader leak tracing, and dominator tree interpretation, refer to our Advanced Heap Dump Analysis Techniques guide.
Diagnostic Tools for Metaspace Analysis
Diagnosing Metaspace problems should involve several JVM artifacts: heap dumps, GC logs, classloader footprints, and native memory usage. The tools below make this process convenient.
Native Memory Tracking (NMT)
Native Memory Tracking (NMT) is one of the most authoritative diagnostics for analyzing Metaspace consumption. It provides subsystem-level visibility into native allocations, including class metadata, thread stacks, code cache, and GC structures.
By enabling NMT, teams can:
- Measure Metaspace reserved vs committed memory
- Track metadata growth trends
- Identify native memory pressure points
- Distinguish Metaspace OOM from overall native exhaustion
NMT can be enabled using:
-XX:NativeMemoryTracking=summary
Or
-XX:NativeMemoryTracking=detail
Allocation statistics can then be retrieved using the following command:
jcmd <pid> VM.native_memory summary
For a deeper understanding of interpreting NMT outputs and allocation categories, refer to our detailed guide on Native Memory Tracking.
Heap Dump Analysis
Heap dumps capture classloader references and metadata footprints, making them essential for identifying:
- Duplicate class definitions
- Stale or leaked classloaders
- Redeploy-related metadata accumulation
Tools such as HeapHero analyze heap dumps to visualize classloader retention chains and pinpoint metadata-heavy components contributing to memory pressure.
JVM & Native Memory Diagnostics
Metaspace resides in native memory, so troubleshooting often requires correlating heap and off-heap signals.
Platforms like yCrash aggregate multiple artifacts, GC logs, thread dumps, heap dumps, and native memory statistics, to provide a unified diagnostic view.
This enables teams to:
- Correlate Metaspace growth with GC activity
- Detect native memory exhaustion patterns
- Identify classloading spikes during deployments
- Accelerate root cause analysis for OutOfMemoryError: Metaspace
Heap dumps capture classloader references and metadata footprints, making them essential for diagnosing Metaspace pressure. If you’re evaluating tooling options, explore our guide on Top Heap Dump Analyzers for Fixing OutOfMemoryError for a comparative overview.
Migration Considerations
Although PermGen was removed automatically in Java 8, migrating legacy applications still requires several operational adjustments.
JVM Parameter Cleanup
PermGen flags like -XX:PermSize and -XX:MaxPermSize are ignored as part of Java 8+ and should be removed. Replace them with -XX:MetaspaceSize and -XX:MaxMetaspaceSize
Native Memory Awareness
Metaspace comes from native memory, so we must monitor more than just heap utilization; systems with limited memory or container memory caps are still able to reach OutOfMemoryError: Metaspace.
Classloader Leak Persistence
Metaspace removes fixed sizing limits but does not eliminate classloader leaks. Continual redeployments or dynamic class generation can also consume metadata memory.
Capacity Planning Shift
Memory planning should be broader and include: Heap, Metaspace, Thread stacks, Direct buffers. This can be particularly important for microservices and containerized environments.
Monitoring Tooling Updates
Legacy heap and PermGen based monitoring is an example from our previous sessions. Metaspace usage, Classloading rates, and Native memory tracking are part of modern observability.
Metaspace vs PermGen: Key Takeaways for Modern Java Applications
The migration from PermGen to Metaspace makes for a significant architectural advance in managing class metadata in the JVM. When metadata storage was moved from a fixed heap region to dynamically scalable native memory in Java 8, one of the biggest causes of memory failures was removed: OutOfMemoryError: PermGen space.
While Metaspace reduces tuning friction and provides good scalability, it does not eliminate the need for monitoring. There is still some very real risk with classloader leaks, excessive dynamic class generation, and native memory exhaustion. The understanding of architectural differences, the proper metrics, and application of diagnostic tooling are important to maintain the JVM’s stability in modern Java environments.

Share your Thoughts!