3 min read

JVM Memory Layout: Heap, Stack & Metaspace Deep Dive

Memory management in the Java Virtual Machine is designed to automate memory allocation and deallocation, freeing developers from manual pointer management (like malloc and free in C). However, building high-throughput enterprise applications requires a precise understanding of JVM runtime data areas, object headers on the heap, stack frame operand operations, and off-heap Metaspace tuning.

In this architecture deep dive, we examine JVM runtime memory regions, break down HotSpot 64-bit object headers, evaluate Thread-Local Allocation Buffers (TLAB), and analyze memory alignment padding.


1. Runtime Data Areas Architecture

When the JVM runs an application, it divides host memory into five distinct runtime data areas categorized into Thread-Private and Thread-Shared memory regions.


flowchart TD
    subgraph SharedMemory["Thread-Shared Memory (Accessible by All Application Threads)"]
        subgraph Heap["Java Heap Space (-Xms / -Xmx)"]
            subgraph YoungGen["Young Generation"]
                Eden["Eden Space (New Allocations)"]
                S0["Survivor Space 0 (S0)"]
                S1["Survivor Space 1 (S1)"]
            end
            OldGen["Old / Tenured Generation (Long-lived Objects)"]
        end
        Metaspace["Metaspace (Off-Heap Native Memory)\nClass Metadata, Constant Pool, Method Bytecodes"]
    end

    subgraph PrivateMemory["Thread-Private Memory (Per Thread Stack)"]
        Stack["Thread Stack Frame (-Xss)\n(Local Variables, Operand Stack, Frame Data)"]
        PC["Program Counter (PC) Register"]
        NativeStack["Native Method Stack (JNI C-Calls)"]
    end

Thread-Private Memory Regions

  • Program Counter (PC) Register: Stores the memory address of the JVM bytecode instruction currently being executed by the thread.
  • Native Method Stack: Stores stack frames for C/C++ native JNI function calls.
  • Java Thread Stack (-Xss): Allocated per thread upon creation. Stores a series of Stack Frames corresponding to active method calls. Each stack frame contains:
    • Local Variable Table (LVT): Stores primitive method arguments and object reference pointers.
    • Operand Stack: Pushdown LIFO workspace for executing arithmetic and bytecode operations.
    • Frame Data: Stores constant pool resolution references and method return addresses.

Thread-Shared Memory Regions

  • Java Heap Space (-Xms / -Xmx): Stores all object instances and array allocations. Divided into Young Generation (Eden, S0, S1) and Old Generation.
  • Metaspace (Native Memory): Replaced PermGen in Java 8. Located in off-heap OS native memory, storing class structures, method bytecodes, annotations, and constant pools. Configured via -XX:MaxMetaspaceSize.

2. HotSpot 64-Bit Object Header Layout

Every object instance allocated on the Java Heap includes a hidden overhead header created by the HotSpot JVM. On a 64-bit JVM, an object header consists of two primary components: the Mark Word and the Klass Word.


block-beta
  columns 1
  mark["Mark Word (8 Bytes / 64 bits)\n(Identity HashCode, GC Age, Biased Lock Flag, Lock State Bits)"]
  klass["Klass Pointer Word (4 Bytes with +UseCompressedClassPointers / 8 Bytes raw)\n(Pointer to Class Metadata in Metaspace)"]
  data["Instance Fields Data\n(Primitive Values & Reference Pointers)"]
  padding["Alignment Padding\n(0 to 7 Bytes to round object size to multiple of 8)"]

The Mark Word (8 Bytes)

The Mark Word is a multi-purpose header field whose interpretation changes dynamically based on the object's state using the bottom 2-3 lock bits:

  • Unlocked State (001): Stores object identity hash code, GC age bits (4 bits, max age 15), and biased lock flag (0).
  • Biased Lock State (101): Stores thread ID pointer of the locked owner thread.
  • Lightweight Lock State (000): Stores pointer to basic object lock in the owner thread stack frame.
  • Heavyweight Lock State (010): Stores pointer to native OS Object Monitor mutex.
  • Marked for GC (011): Used during garbage collection sweep marking.

Memory Alignment Padding

The JVM requires all objects on the heap to begin at memory addresses that are multiples of 8 bytes. If an object's total size (Header + Fields) is 21 bytes, the JVM adds 3 bytes of zero padding to round the allocation to 24 bytes, maximizing CPU L1/L2 cache line alignment speed.


3. Fast Object Allocation: TLAB (Thread-Local Allocation Buffer)

Because the Java Heap is shared across all application threads, concurrent allocation in Eden space would require global lock synchronization, creating severe contention bottlenecks. HotSpot solves this using Thread-Local Allocation Buffers (TLAB).


flowchart LR
    subgraph EdenSpace["Eden Space Memory Region"]
        TLAB1["TLAB Buffer (Thread 1 - Lock Free Allocation)"]
        TLAB2["TLAB Buffer (Thread 2 - Lock Free Allocation)"]
        TLAB3["TLAB Buffer (Thread 3 - Lock Free Allocation)"]
    end

    Thread1["Thread 1 (new Order())"] --> TLAB1
    Thread2["Thread 2 (new Customer())"] --> TLAB2

During thread initialization, the JVM assigns each thread a private dedicated region inside Eden space called a TLAB. When a thread executes new Object(), it allocates memory inside its own private TLAB via a simple bump-the-pointer operation in O(1) time without taking global locks. Only when a thread's TLAB becomes full does it synchronize with the JVM heap to request a new TLAB block.


Related Guides:
← PreviousThe JVM Execution Lifecycle: Bootstrap to ExitNext →JVM Architecture & ClassLoader Subsystem