JVM Heap Memory: Young Gen, Old Gen, Metaspace, and Object Headers

A deep dive into JVM heap memory organization including Young Generation, Old Generation, Metaspace, and object header internals for performance optimization.

published: reading time: 20 min read author: GeekWorkBench

JVM Heap Memory: Young Gen, Old Gen, Metaspace, and Object Headers

If you have ever stared at a OutOfMemoryError: Heap Space stack trace and had no idea where to start, you are not alone. The JVM heap looks simple from the outside - just memory for objects - but dig a little deeper and you will find a carefully engineered layout designed to make garbage collection faster and more predictable. This post walks through how the heap is actually organized, what Metaspace is doing in native memory, and how object headers work at the bit level.

Introduction

The JVM heap is the managed memory space where every Java object lives, yet its internal organization is something most developers never think about until things go wrong. The heap is split into generations—Young Generation for short-lived objects and Old Generation for long-lived survivors—each with different garbage collection strategies and performance characteristics. Metaspace lives separately in native memory, storing class metadata that many developers confuse with heap memory when they see an OutOfMemoryError: Metaspace. Understanding this layout is not academic: it directly explains why OutOfMemoryError: Heap Space behaves differently from OutOfMemoryError: Metaspace, why minor GC reclaims most objects while major GC takes longer, and why tuning SurvivorRatio changes application performance in ways that seem counterintuitive.

Object headers are the hidden metadata bolted onto every object—typically 12 bytes on a 64-bit JVM with Compressed OOPs. The mark word stores hash codes, age, and lock state; the klass pointer points to class metadata in Metaspace. These internals matter when you are staring at a heap dump, trying to understand why a 17-byte object actually consumes 24 bytes, or when the JVM is spending unexpected CPU time on GC betweensafepoint operations.

This post walks through heap organization in detail: how objects flow from Eden to Survivor spaces to Old Generation, what actually lives in Metaspace, and how object headers work at the bit level. You will learn to read GC logs in context of the heap layout, size heaps appropriately for your workload, and understand what the JVM is actually doing when it complains about memory.

When to Use This Knowledge

Use when:

  • Diagnosing OutOfMemoryError: Heap Space errors
  • Tuning garbage collector settings for specific workloads
  • Analyzing memory dumps (heap dumps, hprof files)
  • Optimizing applications with large object graphs or high allocation rates
  • Choosing between different GC algorithms based on heap behavior

Do not use when:

  • Writing simple applications with predictable memory usage
  • Using managed languages/platforms that abstract away memory details
  • Debugging issues unrelated to memory (e.g., CPU bottlenecks)

When NOT to Use This Knowledge

If you are working on short-lived applications with predictable allocation patterns, the JVM defaults handle memory management fine. Tuning SurvivorRatio and tenuring thresholds for a batch job that runs once daily and exits is premature optimization that adds complexity without measurable benefit.

Most applications never need custom heap tuning. Modern GC collectors like G1 and ZGC self-tune effectively for most workloads. If you are not hitting memory errors in production, the default heap settings are probably fine.

In managed environments like Kubernetes, or when using a platform-as-a-service provider that abstracts memory configuration, deep heap knowledge has limited practical value. Focus on application-level concerns like algorithm efficiency and avoiding unnecessary allocations instead. For continued learning on JVM tuning, explore the Advanced Java & JVM Internals roadmap.

JVM Heap Memory Architecture

The heap is split into a few distinct regions, each handling a different phase of an object’s life.

graph TB
    subgraph JVMHeap["JVM Heap Memory"]
        subgraph YoungGen["Young Generation"]
            subgraph Eden["Eden Space"]
                E["Objects allocated here"]
            end
            subgraph SurvivorS["Survivor Space S0"]
                S0["Survivors after minor GC"]
            end
            subgraph SurvivorS1["Survivor Space S1"]
                S1["Survivors after minor GC"]
            end
        end
        subgraph OldGen["Old Generation"]
            OG["Long-lived objects promoted from Young Gen"]
        end
    end
    subgraph NativeMemory["Native Memory"]
        MS["Metaspace - Class Metadata"]
        CCS["Compressed Class Space"]
        TH["Thread Stacks"]
        DM["Direct Memory Buffers"]
    end

    E -->|"Minor GC| Aging"| S0
    E -->|"Minor GC| Aging"| S1
    S0 -->|"After 15 GCs"| OG
    S1 -->|"After 15 GCs"| OG

    class E eden
    class S0,S1 survivor
    class OG old
    class MS,CCS,TH,DM native

Young Generation Details

New objects land in the Young Generation. It has three parts:

  • Eden Space: Where allocations start. Most objects die here before they ever get promoted anywhere.
  • Survivor Spaces (S0 and S1): Two regions that hold objects that survive a minor GC. Objects cycle between S0 and S1, getting older with each pass - this is called “aging.”

Object Allocation Flow

  1. New object allocated in Eden space
  2. Minor GC runs - live objects are copied to S0 (or S1)
  3. Objects in S0 age and are copied to S1 during next minor GC
  4. This ping-pong process continues
  5. Objects that reach the tenuring threshold (default: 15) are promoted to Old Generation

The tenuring threshold is configurable via -XX:MaxTenuringThreshold=N where N ranges from 1 to 15 (or 65535 with UseAdaptiveSizePolicy).

Old Generation Details

Once objects have aged out in the Survivor spaces, they end up here. The Old Generation (sometimes called Tenured Generation) is where long-lived objects go to retire.

Characteristics:

  • Larger than Young Generation, usually 2-3x
  • GC runs less often here, but when it does, it takes longer
  • Supports different GC algorithms than Young Generation
  • Objects here tend to stick around

When objects get promoted:

  • They outgrow the Survivor spaces
  • They hit the tenuring threshold (default: 15 minor GCs)
  • They are large enough to bypass Young Generation entirely (with -XX:PretenureSizeThreshold)

Metaspace vs Heap

Metaspace is not on the heap at all. It lives in native memory, which trips up a lot of people who see OutOfMemoryError: Metaspace and assume they need to bump -Xmx.

graph LR
    subgraph NativeMemory["Native Memory"]
        MS["Metaspace"]
        CCS["Compressed Class Space"]
        TS["Thread Stacks"]
    end

    class MS,CCS,TS native

Metaspace

Metaspace holds:

  • Class metadata (class name, modifiers, field info, method signatures)
  • Constant pools
  • JIT compiled code
  • Internal JVM structures

Heap vs Metaspace at a glance:

AspectHeapMetaspace
What lives hereObject instancesClass metadata
WhereJava managed memoryNative memory
GCRegular heap GCOwn metadata GC
OOM typeHeap SpaceMetaspace
ResizeVia -XmxGrows by default, capped by OS

Metaspace configuration:

  • -XX:MetaspaceSize - Initial size before first GC
  • -XX:MaxMetaspaceSize - Maximum (unlimited by default)
  • -XX:MinMetaspaceFreeRatio - Minimum free space after GC

Compressed Class Space

64-bit JVMs with Compressed OOPs need a separate space for class metadata that can be compressed. Set with -XX:CompressedClassSpaceSize (default: 1GB).

Object Header Layout

Every object has a header bolted onto the front of it. Most people never think about this until they are staring at a heap dump or using a tool like JOL.

Mark Word (64 bits / 8 bytes on 64-bit JVM)

|-----------------------|--------------|-------------|
|        Hash Code      |    Age       |  Lock State |
|        (25 bits)      |   (4 bits)   |  (2 bits)   |
|------------------------------------------------------|
|                    Metadata                           |
|                    (23 bits)                          |
|------------------------------------------------------|
|                   Object Reference                    |
|                    (64 bits)                          |
|------------------------------------------------------|

The mark word stores:

  • Identity hash code (computed on demand)
  • Age (minor GCs survived)
  • Lock state (unlocked, biased, thin, or fat)
  • GC metadata

Klass Pointer (64 bits / 8 bytes on 64-bit JVM with Compressed OOPs)

Points to the class metadata in Metaspace. With Compressed Object Pointers (OOPs), this is 4 bytes (32 bits).

Array Length (32 bits / 4 bytes) - Arrays only

Just the length. Arrays get this extra field; regular objects do not.

Total Object Header Size

JVM ModeObject Header Size
32-bit JVM8 bytes (mark) + 4 bytes (klass) = 12 bytes
64-bit JVM8 bytes (mark) + 8 bytes (klass) = 16 bytes
64-bit with Compressed OOPs8 bytes (mark) + 4 bytes (klass) = 12 bytes
Array (64-bit with OOPs)8 + 4 + 4 = 16 bytes

Production Failure Scenarios

These are the issues I see most often in production.

1. Premature Promotion (Promotion Failure)

Symptom: Frequent Full GC events even though Old Generation is not full.

Cause: Large objects in Young Generation that survive minor GC cause immediate promotion due to -XX:PretenureSizeThreshold or Survivor Space exhaustion.

Solution:

# Increase Survivor spaces
-XX:SurvivorRatio=6
-XX:MaxTenuringThreshold=15

# Or increase total Young Generation
-Xmn512m  # Set Young Generation size

2. Metaspace Exhaustion

Symptom: OutOfMemoryError: Metaspace even though your heap has plenty of room.

Cause: Class loader leaks or frameworks that generate many classes at runtime (OSGi, JSP containers, dynamic proxies, CGLIB).

Solution:

# Set Metaspace limit
-XX:MaxMetaspaceSize=256m

# Monitor class loading
jstat -gc 12078 1000

3. Heap Fragmentation

Symptom: OutOfMemoryError: Heap Space but the heap dump shows plenty of free space scattered around.

Cause: Mark-Sweep leaves gaps. When a large object needs contiguous memory, it fails even though total free memory is sufficient.

Solution:

  • Use G1 or ZGC with better compaction
  • Increase -XX:MinHeapFreeRatio and -XX:MaxHeapFreeRatio

Trade-off Table

These are the knobs I reach for most often when tuning heap:

ConfigurationDefaultBenefitCost
-Xms / -Xmx1/64th of RAMPredictable memory footprintWasted if overprovisioned
-Xmn (Young Gen Size)DynamicControl minor GC frequencyMay starve Old Gen
-XX:SurvivorRatio8 (Eden/Survivor)Tune how long objects ageToo high wastes space
-XX:MaxTenuringThreshold15Longer aging delays promotionCan flood Old Gen if too high
-XX:MetaspaceSizePlatform dependentDelays first Metaspace GCUses more native memory

Implementation Snippets

Checking Heap Usage

import java.lang.management.*;

public class HeapMemoryMonitor {
    public static void main(String[] args) {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();

        System.out.println("Heap Memory:");
        System.out.printf("  Used: %d MB%n", heapUsage.getUsed() / 1024 / 1024);
        System.out.printf("  Max: %d MB%n", heapUsage.getMax() / 1024 / 1024);
        System.out.printf("  Committed: %d MB%n", heapUsage.getCommitted() / 1024 / 1024);

        System.out.println("Non-Heap Memory (Metaspace):");
        System.out.printf("  Used: %d MB%n", nonHeapUsage.getUsed() / 1024 / 1024);
    }
}

Analyzing Object Header with JOL

import org.openjdk.jol.info.*;
import org.openjdk.jol.vm.*;

public class ObjectHeaderAnalysis {
    public static void main(String[] args) {
        Object obj = new Object();

        ClassLayout layout = ClassLayout.parseInstance(obj);
        System.out.println(layout.toPrintable());
    }
}

Maven dependency:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

Observability Checklist

  • Monitor heap with jstat -gc <pid>
  • Enable GC logging: -Xlog:gc*
  • Track native memory: jcmd <pid> VM.native_memory summary
  • Dump the heap when you need to investigate: jmap -dump:file=heap.hprof <pid>
  • Watch Metaspace in jstat -gc <pid> output (columns mc and ccs)
  • Set up Flight Recorder for allocation profiling
  • Look for frequent Full GC in your GC logs
  • Check compressed class pointer mode with -XX:+PrintCompressedOopsMode

Security Notes

  1. Heap Dumps can contain PII, passwords, tokens - treat them like sensitive data
  2. Native Memory Access (Unsafe) lets you read/write heap directly - lock down module access in production
  3. Memory Leaks can exhaust a containerized pod - set resource limits
  4. Class Structure in heap dumps can reveal implementation details to attackers

Common Pitfalls / Anti-Patterns

PitfallWhat happensFix
-Xmx set to container limitOOMKilled because OS, Metaspace, direct buffers need memory tooLeave headroom
Ignoring Metaspace sizingMetaspace OOM in productionSet -XX:MaxMetaspaceSize
Using default SurvivorRatioPoor aging behavior for your workloadTune based on allocation rate
Forgetting heap is fragmentedMark-Sweep leaves gapsSwitch to G1 or ZGC
Young gen too smallMinor GC fires constantlyBump -Xmn or -XX:NewRatio

Quick Recap Checklist

  • Heap = Young Generation (Eden + S0 + S1) + Old Generation
  • Metaspace lives in native memory, stores class metadata
  • Object header = Mark Word + Klass Pointer (+ array length for arrays)
  • Compressed OOPs on 64-bit JVM = 12-byte headers instead of 16
  • Young gen sizing controls minor GC frequency and promotion rate
  • Metaspace grows by default; set -XX:MaxMetaspaceSize if you need a cap
  • jstat, GC logs, and heap dumps are your main debugging tools
  • Tune -Xms, -Xmx, -Xmn based on your workload

Interview Questions

1. Why is the JVM heap divided into Young and Old generations?

Most objects die young. By keeping the young generation small and collecting it frequently with a fast copying collector, the JVM handles the majority of garbage at low cost. Objects that survive long enough get promoted to Old Generation, which uses a different (slower but thorough) collector. This is the classic generational hypothesis in action.

2. What is the difference between Mark-Sweep and Mark-Compact garbage collection?

Mark-Sweep marks reachable objects, then sweeps up the rest. The free memory ends up fragmented. Mark-Compact adds a compact phase that slides live objects together, eliminating the gaps. Compact is more expensive CPU-wise but prevents fragmentation issues that lead to allocation failures even when total free memory looks fine.

3. What causes premature promotion in the JVM?

Objects get promoted before they should when the Survivor spaces overflow. This happens when S0/S1 are too small for your allocation rate, or when large objects are allocated directly in Old Generation via -XX:PretenureSizeThreshold. The result is more Full GCs than you would expect from your object lifetimes alone.

4. What is Metaspace and how does it differ from the Java heap?

Metaspace lives in native memory, not on the heap. It stores class metadata - class definitions, method info, constant pools, JIT compiled code. The heap stores object instances. When Metaspace runs out, you get OutOfMemoryError: Metaspace, which you cannot fix by increasing -Xmx. Set -XX:MaxMetaspaceSize if you want an upper bound.

5. How does Compressed OOPs affect object header size?

With compressed OOPs (the default on 64-bit JVMs with heap under 32GB), object references compress from 8 bytes to 4 bytes. The Klass pointer also shrinks from 8 to 4 bytes. This cuts the header from 16 bytes down to 12 bytes for regular objects. The CPU cost of compressing and decompressing references is negligible; the memory savings are substantial for reference-heavy applications.

6. What is the purpose of Survivor spaces in the Young Generation?

Survivor spaces (S0 and S1) provide a holding area for objects that survive minor GC before they are old enough to promote to Old Generation. Objects age by copying between S0 and S1 with each minor GC. This prevents short-lived objects from flooding Old Generation and reduces the number of Full GCs. The tenuring threshold determines how many minor GCs an object must survive before promotion.

7. How does the generational hypothesis influence GC algorithm choices?

The generational hypothesis states that most objects die young. This justifies using a fast copying collector for young generation (where most garbage is) and a more thorough Mark-Compact collector for old generation (where survivors accumulate). Copying is ideal for young gen because objects die there quickly, meaning less copying work overall. Mark-Compact suits old gen because objects stick around and need efficient compaction to avoid fragmentation.

8. What causes Metaspace to grow unbounded in production?

Class loader leaks are the usual culprit. Applications using OSGi, dynamic proxies, JSP containers, or frameworks that generate classes at runtime (CGLIB, bytecode generation) can accumulate class loaders that never become unreachable. Each class loader holds references to all classes it loaded. The fix is either to set -XX:MaxMetaspaceSize as a hard limit, or to find and fix the class loader leak. Use jstat -gc to monitor Metaspace growth and jmap -clstats to find class loader leaks.

9. What is the difference between -Xms and -Xmx?

-Xms sets the initial heap size at JVM start. -Xmx sets the maximum heap size the JVM can grow to. In production, setting them equal (-Xms=-Xmx) eliminates heap resizing overhead and prevents pause spikes from resize events. If they differ, the JVM will grow or shrink the heap dynamically, which triggers GC cycles and creates unpredictable pauses.

10. Why do 64-bit JVMs use Compressed OOPs?

Without compression, object references on 64-bit JVMs take 8 bytes each, which increases memory usage significantly for reference-heavy applications. Compressed OOPs (enabled by default for heaps under 32GB) packs references into 4 bytes by using 35-bit addressing (covering up to 32GB) plus small adjustments. Above 32GB heap, compression must be disabled and references use full 8 bytes. The trade-off is a small CPU cost for compression/decompression, which is negligible compared to the memory savings.

11. What is object header fragmentation in the context of heap memory?

Object header fragmentation refers to wasted space due to alignment padding and the fact that headers are fixed-size even when not all fields are needed. The JVM aligns objects to 8-byte boundaries, so a 17-byte object actually uses 24 bytes. Additionally, every object has an 8-16 byte header regardless of actual content. In large object graphs with many small objects, this overhead compounds significantly.

12. How does -XX:PretenureSizeThreshold work?

This flag causes objects larger than the threshold to be allocated directly in Old Generation instead of Eden. This is useful for large, long-lived objects like caches or connection pools that would otherwise cause frequent minor GCs and Survivor space pressure. Setting this incorrectly causes premature Old Generation fill, so it should only be used after analyzing allocation patterns with a profiler.

13. What is the difference between Shallow Heap and Retained Heap in memory analysis?

Shallow heap is the size of the object itself (object header plus fields), not including anything it references. Retained heap is the total memory that would be freed if the object and all objects it references (directly or indirectly) were collected. Retained heap matters because a small object holding references to a large structure keeps that entire structure alive.

14. What is the relationship between heap size and GC pause times?

Larger heaps hold more live objects, which means longer pause times during stop-the-world GC phases (Mark and Compact). However, larger heaps also mean fewer GC cycles for the same amount of work. The optimal heap size balances frequency against duration of pauses. For low-latency applications, keep heaps smaller and use collectors like G1, ZGC, or Shenandoah that do work concurrently rather than in long stop-the-world phases.

15. Why might an OutOfMemoryError occur even when heap usage appears low?

Fragmentation is the most common cause. Pure Mark-Sweep leaves free memory as scattered islands. A 500MB allocation might fail because free memory exists but not contiguously. This is why collectors with compaction (G1, ZGC, Shenandoah, Mark-Compact) prevent this scenario. Another cause is Metaspace exhaustion - it lives outside the heap, so heap metrics look fine while native memory is depleted.

16. What is card marking and how does it optimize minor GC in generational collectors?

Card marking divides the old generation into 512-byte cards. When a reference from old gen to young gen is modified, the card is marked dirty. During minor GC, instead of scanning the entire old generation to find cross-generational references, only dirty cards are scanned. This dramatically reduces minor GC pause time when the heap is large. The card table is a side data structure maintained by the write barrier when mutator threads modify object references.

17. What is TLAB (Thread Local Allocation Buffer) and how does it reduce allocation contention?

TLAB grants each thread a private allocation buffer in Eden space. Threads allocate from their TLAB using a bump-the-pointer technique that requires no synchronization for local allocations. Only when a TLAB is exhausted does the thread need to get a new TLAB from the global allocator, which requires a small atomic operation. This design eliminates contention on the global allocation path for the common case of thread-local allocation, significantly improving throughput under multi-threaded allocation.

18. How does object header layout differ between 32-bit and 64-bit JVMs with and without Compressed OOPs?

On a 32-bit JVM, an object header is 8 bytes (mark word only). On a 64-bit JVM without compressed OOPs, the mark word is 8 bytes, the klass pointer is 8 bytes, totaling 16 bytes. With compressed OOPs on a 64-bit JVM, the klass pointer compresses to 4 bytes, making the total header 12 bytes for regular objects and 16 bytes for arrays. The JVM uses compressed OOPs when the heap is under 32GB because 35-bit addressing can address up to 32GB using small oop displacements.

19. How does the promotion threshold (MaxTenuringThreshold) affect object promotion timing?

MaxTenuringThreshold determines how many minor GC cycles an object must survive in Survivor spaces before being promoted to old generation. The default is 15. With UseAdaptiveSizePolicy enabled, the JVM adaptively adjusts this threshold based on allocation and survival rates. A higher threshold means objects age longer before promotion, which is good if many long-lived objects die young. A lower threshold promotes objects earlier, which is useful if Survivor spaces are filling up. The threshold is per-object, tracked via the age field in the mark word.

20. What is the relationship between heap size, Survivor spaces, and promotion rate?

The Survivor spaces (S0 and S1) provide buffering between Eden and old gen. If Survivor spaces are too small relative to Eden, objects overflow directly to old gen instead of aging properly, flooding old gen with short-lived objects. The promotion rate is determined by the ratio of Survivor space size to Eden size multiplied by the allocation rate. For example, with SurvivorRatio=8 and a young gen of 1GB, each Survivor is 100MB. Tuning this ratio controls how long objects age before promotion.

Further Reading

Conclusion

The JVM heap divides into Young Generation (Eden + S0 + S1) for short-lived objects and Old Generation for long-lived survivors. Metaspace lives in native memory and stores class metadata. Object headers consist of a Mark Word, Klass Pointer, and (for arrays) a length field; compressed OOPs reduce header size from 16 to 12 bytes on 64-bit JVMs under 32GB heap. Size heap with -Xms=-Xmx for predictability and tune SurvivorRatio and tenuring threshold based on your allocation rate.

Category

Related Posts

CMS and G1 Collectors: Low-Latency Garbage Collection

How CMS and G1 garbage collectors reduce pause times through concurrent marking, region-based heap layout, and incremental compaction.

#jvm #garbage-collection #cms

GC Fundamentals: Mark-Compact, Copying, and Mark-Sweep

Understanding the three core garbage collection algorithms - Mark-Sweep, Mark-Compact, and Copying - their mechanics, trade-offs, and when to use each.

#jvm #garbage-collection #gc-algorithms

JVM GC Tuning: Heap Sizing and Threshold Optimization

Practical strategies for sizing JVM heap, tuning generation ratios, and optimizing GC thresholds to reduce pause times and improve throughput.

#jvm #garbage-collection #heap-tuning