Java Metaspace vs PermGen
Java Metaspace vs PermGen
PermGen and Metaspace are two different answers to the same question: where does the JVM keep a loaded class's metadata — its structure, its method bytecode, its constant pool — while the class is in use. PermGen answered that question through Java 7, as a fixed-size region bolted onto the heap. Metaspace, introduced in Java 8, answered it by moving that data into native memory that grows on its own, and in doing so quietly ended one of the most common production incidents in Java's history.
What Were PermGen and Metaspace?
Both are JVM memory regions that store class-level data rather than object instances — the compiled bytecode for each method, the constant pool, static references, and the metadata describing a class's own shape. Neither one holds the objects your application creates at runtime; that has always been the heap's job.
The difference is where that class data physically lives and how its size is managed. PermGen was part of the JVM heap itself, sized with a small, fixed default that frequently needed manual tuning. Metaspace lives in native memory, allocated directly from the operating system, and grows automatically as more classes are loaded, with no fixed cap unless one is explicitly configured.
Why Metaspace Replaced PermGen
An application that generates many classes at runtime — Hibernate building proxy classes for entities, Spring building proxies for AOP-advised beans, a JSP container compiling a new class per page, a plugin system loading business logic dynamically — accumulates class metadata over its lifetime. PermGen's small, fixed default size meant this kind of application could exhaust it entirely, producing one of the most recognizable errors in Java's history:
java.lang.OutOfMemoryError: PermGen space
This was especially common in application servers under repeated hot-redeploys, since old classloaders — and every class they had loaded — sometimes lingered instead of being cleanly unloaded, pushing PermGen usage a little higher with each redeploy until it finally ran out. Manually raising -XX:MaxPermSize bought time but did not fix the underlying constraint: PermGen still had a ceiling, and busy applications kept finding it.
JEP 122 replaced PermGen with Metaspace in Java 8. By moving class metadata into native memory instead of a fixed region of the heap, the JVM removed that ceiling for the common case — Metaspace grows as classes are loaded, limited only by how much memory the operating system can actually provide, unless a cap is set deliberately.
| Aspect | PermGen (through Java 7) | Metaspace (Java 8 onward) |
|---|---|---|
| Location | Part of the JVM heap | Native memory, allocated from the OS |
| Default size limit | Small and fixed, often needed manual tuning | No fixed cap by default — grows with available native memory |
| Common failure | OutOfMemoryError: PermGen space, frequent in proxy-heavy or hot-redeploy environments | OutOfMemoryError: Metaspace, far less common under default settings |
| Tuning flag | -XX:MaxPermSize | -XX:MaxMetaspaceSize (optional) |
| What it stores | Class metadata, method bytecode, constant pool | The same category of data — class metadata, method bytecode, constant pool |
| Interned Strings | Not stored here in later Java 7 releases — moved to the heap before Metaspace existed | Never stored here either, for the same reason |
| Reclaimed by GC | Yes, as part of full garbage collection cycles | Yes, when the classloader that defined those classes becomes unreachable |
One sentence before the diagram: the two designs put the same category of data in physically different places relative to the heap.
Java 7 and earlier Java 8 and later
JVM Heap JVM Heap
+-------------------+ +-------------------+
| Young Generation | | Young Generation |
| Old Generation | | Old Generation |
| PermGen | +-------------------+
| (class metadata) |
+-------------------+ Native (off-heap) memory
+-------------------+
| Metaspace |
| (class metadata) |
+-------------------+
PermGen sat inside the same contiguous heap that objects live in, sharing its fate with heap sizing decisions. Metaspace sits entirely outside the heap, which is exactly why it is no longer constrained by heap-sizing flags at all.
How Metaspace Works Internally
Every class a classloader loads gets its metadata stored in Metaspace the moment the class is defined — this happens as part of the class-loading process, before any instance of that class is ever created. That metadata stays in Metaspace for as long as the defining classloader remains reachable. When a classloader becomes unreachable — most commonly because whatever created it, such as a redeployed web application context, is itself discarded — every class it loaded becomes eligible for unloading, and the garbage collector reclaims that Metaspace memory during a collection cycle.
java.lang.reflect.Proxy is a clear, safe way to see class accumulation happen directly, since each distinct interface combination it is asked to proxy produces its own generated class — but the same interface asked for twice returns the same cached class rather than generating a duplicate.
1// File: DynamicProxyClassExample.java
2import java.lang.reflect.*;
3
4public class DynamicProxyClassExample {
5
6 interface Greeter {
7 String greet(String name);
8 }
9
10 public static void main(String[] args) {
11 Greeter proxyOne = (Greeter) Proxy.newProxyInstance(
12 DynamicProxyClassExample.class.getClassLoader(),
13 new Class<?>[] { Greeter.class },
14 (proxy, method, methodArgs) -> "Hello, " + methodArgs[0]);
15
16 Greeter proxyTwo = (Greeter) Proxy.newProxyInstance(
17 DynamicProxyClassExample.class.getClassLoader(),
18 new Class<?>[] { Greeter.class },
19 (proxy, method, methodArgs) -> "Hi, " + methodArgs[0]);
20
21 System.out.println(proxyOne.greet("Ananya"));
22 System.out.println("Same generated class: " + (proxyOne.getClass() == proxyTwo.getClass()));
23 }
24}Output:
Hello, Ananya
Same generated class: true
proxyOne and proxyTwo both implement the same single interface, Greeter, through the same classloader, so Proxy reuses the exact same generated class for both — only their invocation handlers differ. A different interface, or a different combination of interfaces, produces a genuinely new class instead, which is exactly the pattern behind the real-world example below.
Real-World Example
A logging-proxy factory wraps service interfaces with a dynamic proxy that logs every call before delegating to the real implementation — a common pattern in frameworks like Spring AOP, and exactly the kind of code that generates a new class per distinct interface over an application's lifetime.
1// File: ServiceProxyFactory.java
2import java.lang.reflect.*;
3
4public class ServiceProxyFactory {
5
6 @SuppressWarnings("unchecked")
7 public static <T> T createLoggingProxy(Class<T> serviceInterface, T target) {
8 return (T) Proxy.newProxyInstance(
9 serviceInterface.getClassLoader(),
10 new Class<?>[] { serviceInterface },
11 (proxy, method, args) -> {
12 System.out.println("Calling " + method.getName());
13 return method.invoke(target, args);
14 });
15 }
16}1// File: ProxyClassAccumulationDemo.java
2
3public class ProxyClassAccumulationDemo {
4
5 interface OrderProcessor {
6 void process(String orderId);
7 }
8
9 interface InvoiceProcessor {
10 void process(String invoiceId);
11 }
12
13 static class OrderProcessorImpl implements OrderProcessor {
14 public void process(String orderId) {
15 System.out.println("Order " + orderId + " processed");
16 }
17 }
18
19 static class InvoiceProcessorImpl implements InvoiceProcessor {
20 public void process(String invoiceId) {
21 System.out.println("Invoice " + invoiceId + " processed");
22 }
23 }
24
25 public static void main(String[] args) {
26 OrderProcessor orderProxy = ServiceProxyFactory.createLoggingProxy(
27 OrderProcessor.class, new OrderProcessorImpl());
28 InvoiceProcessor invoiceProxy = ServiceProxyFactory.createLoggingProxy(
29 InvoiceProcessor.class, new InvoiceProcessorImpl());
30
31 orderProxy.process("ORD-501");
32 invoiceProxy.process("INV-501");
33
34 System.out.println("Distinct proxy classes generated: " +
35 (orderProxy.getClass() != invoiceProxy.getClass()));
36 }
37}Output:
Calling process
Order ORD-501 processed
Calling process
Invoice INV-501 processed
Distinct proxy classes generated: true
OrderProcessor and InvoiceProcessor are different interfaces, so each proxy request generates its own distinct class, and each one occupies its own slice of Metaspace for as long as its classloader stays reachable. A mistake that appears often in fresher pull requests is treating this kind of dynamic proxy generation as free, when a real application with dozens of proxied service interfaces — and a redeploy cycle that does not always cleanly release old classloaders — is accumulating exactly this kind of class metadata over time. This is precisely the growth pattern that used to exhaust PermGen's small fixed cap, and precisely why Metaspace's native, auto-growing design made that specific failure so much rarer.
Best Practices
Leave -XX:MaxMetaspaceSize unset unless production monitoring has actually shown unbounded class-metadata growth — the default unbounded-until-native-memory-limit behavior is usually safer than an arbitrarily chosen cap.
Remove leftover -XX:PermSize and -XX:MaxPermSize flags from deployment scripts written for pre-Java-8 applications — they no longer apply and only add confusion for whoever reads the startup command next.
Monitor Metaspace usage in any environment with frequent hot-redeploys or dynamic class generation, the same way heap usage gets monitored, since a classloader leak can still exhaust Metaspace even though it lives off-heap.
Treat a growing number of loaded classes over an application's lifetime as a signal worth investigating, not something to dismiss just because Metaspace rarely runs out under normal conditions.
Common Mistakes
Assuming Metaspace has no size limit at all, and can never run out of memory, overlooks that it is still bounded by whatever native memory is actually available on the machine, or by -XX:MaxMetaspaceSize if one is set — OutOfMemoryError: Metaspace is a real, still-possible error, just triggered far less often than PermGen's small fixed cap was under normal application behavior.
Assuming interned Strings live in Metaspace is an understandable mix-up, since they did live in PermGen at one point — but the String pool was moved out to the regular heap back in Java 7, a full release before Metaspace even existed, so Metaspace has never actually stored interned Strings at all.
Interview Questions
Q1. What was PermGen, and why was it removed in Java 8?
PermGen was the JVM memory region, part of the heap, that stored class metadata — method bytecode, the constant pool, and class structure — through Java 7. It was replaced because its small, fixed default size made it a frequent source of OutOfMemoryError: PermGen space in applications that generated many classes at runtime, and manual tuning only delayed the problem rather than solving it. Interviewers listen for whether you know this was a genuine, common production incident, not just a textbook footnote.
Q2. Where does Metaspace live, and how is that different from where PermGen lived?
Metaspace lives in native memory, allocated directly from the operating system, entirely outside the JVM heap. PermGen lived inside the heap itself, which tied its available space to heap-sizing decisions in a way Metaspace no longer is. This distinction is the core of the whole redesign, and interviewers are listening for that "off-heap versus on-heap" framing specifically.
Q3. Does Metaspace have a size limit?
By default, no fixed limit — it grows as classes are loaded, bounded only by the native memory actually available on the machine. A limit can be imposed explicitly with -XX:MaxMetaspaceSize, but unlike PermGen, that cap is opt-in rather than an unavoidable default.
Q4. What is stored in Metaspace?
Class-level metadata: each loaded class's structure, its compiled method bytecode, and its constant pool. It never stores actual object instances, which continue to live on the heap exactly as before.
Q5. Are interned Strings stored in Metaspace?
No. The String pool moved to the regular heap in Java 7, before Metaspace was introduced in Java 8, so Metaspace has never held interned Strings — a detail that trips up candidates who assume it inherited everything PermGen used to store.
Q6. What common production error was associated with PermGen, and what typically caused it?
OutOfMemoryError: PermGen space, typically caused by an application generating many classes at runtime — proxy classes from Hibernate or Spring AOP, dynamically compiled JSP classes, or classes loaded by classloaders that were not being cleanly released across repeated redeploys.
Q7. Can Metaspace still run out of memory in a Java 8+ application?
Yes. A classloader leak, where classloaders and the classes they defined never become unreachable across many redeploys, can still exhaust Metaspace given enough repetitions — moving to native memory removed PermGen's small fixed ceiling, but it did not make unbounded class accumulation harmless.
FAQs
Which Java version replaced PermGen with Metaspace?
Java 8, via JEP 122. PermGen was present through Java 7 and removed entirely starting with Java 8.
Is -XX:MaxPermSize still a valid JVM flag in Java 8 and later?
It is accepted but ignored, with a warning printed at startup, since the setting it controls no longer applies to how the JVM manages class metadata.
Does Metaspace participate in garbage collection?
Yes. When a classloader becomes unreachable, every class it defined becomes eligible for unloading, and a garbage collection cycle reclaims the Metaspace memory those classes occupied — the same underlying idea PermGen used, just relocated to native memory.
Why did PermGen frequently run out of space in Spring and Hibernate applications specifically?
Both frameworks generate proxy classes at runtime — Spring for AOP-advised beans, Hibernate for lazy-loaded entity proxies — and each distinct class or interface combination being proxied adds a new class to PermGen. Applications with many entities or many advised beans accumulated class metadata quickly, especially across repeated redeploys in a development or staging environment.
Is Metaspace part of the Java heap?
No. This is the central change JEP 122 made — Metaspace is native memory managed outside the heap entirely, which is why it is not affected by -Xmx or other heap-sizing flags.
What JEP introduced Metaspace?
JEP 122, part of Java 8, titled "Remove the Permanent Generation."
Does a class ever get removed from Metaspace once loaded?
Yes, but only when its defining classloader becomes unreachable and is collected. Classes loaded by the bootstrap or application classloader effectively never get unloaded, since those classloaders live for the JVM's entire lifetime — it is specifically classes loaded by a custom or transient classloader, such as one created for a hot-redeploy or a plugin system, that can actually be unloaded.
Summary
PermGen and Metaspace both exist to hold the same kind of data — class metadata, not object instances — but Metaspace's move into native, auto-growing memory is what ended the small-fixed-cap failure mode that made OutOfMemoryError: PermGen space one of the most recognizable production errors in pre-Java-8 applications. The underlying mechanism for reclaiming that memory, tied to classloader reachability, has not changed; only where the memory physically lives has.
The habit worth carrying forward from this article's proxy-generation example is recognizing that dynamic class generation is not free, even under Metaspace's more forgiving default — a classloader leak in a hot-redeploy-heavy environment can still exhaust it, just less easily than it used to exhaust PermGen.
What to Read Next
Learn how Java loads your classes into memory.