What is the difference between Heap and Stack
The Heap and the Stack are two fundamentally distinct regions of physical memory allocated by the operating system to the Java Virtual Machine (JVM). Their separation is driven...
🟢 Junior Level
The Heap and the Stack are two fundamentally distinct regions of physical memory allocated by the operating system to the Java Virtual Machine (JVM). Their separation is driven by two distinct operational needs: managing the execution lifecycle of code (threads and method invocations) versus storing application state (objects and data structures).
Real-World Analogy
- The Stack (Personal Workbench): A developer’s private workbench. It holds only the active tools, blueprints, and local variables needed for the immediate task at hand. When the task finishes, the workbench is wiped clean in a single physical sweep. Space is strictly limited, but everything is within arm’s reach.
- The Heap (Central Warehouse): A massive shared enterprise warehouse. It houses all finished machines, bulk materials, and raw inventory containers (
newobjects). All factory workers share access to it, locating items requires address references, and cleaning up abandoned items is handled periodically by a specialized custodial crew (Garbage Collector).
Core Comparison Matrix
| Dimension | Stack Memory | Heap Memory |
|---|---|---|
| What is Stored? | Method stack frames, primitive local variables, object reference pointers | Object instances (new), arrays, String pool, class metadata |
| Visibility Scope | Thread-Private (Strictly isolated per thread; inherently thread-safe) | Globally Shared across all application threads |
| Memory Management | Deterministic LIFO deallocation (hardware CPU stack pointer adjustment) | Managed automatically by the Garbage Collector (GC pauses) |
| Allocation Speed | Instantaneous (single hardware CPU instruction) | Very fast via TLABs; requires GC sweeps when exhausted |
| Default Size | Small (typically 1 MB per thread on 64-bit OS) | Large (gigabytes, configured via JVM flags) |
| JVM Sizing Flags | -Xss (e.g., -Xss1m) |
-Xms (initial) and -Xmx (maximum) |
| Failure Mode | java.lang.StackOverflowError (infinite recursion) |
java.lang.OutOfMemoryError: Java heap space |
Code Execution Memory Allocation
public class MemoryDemo {
public static void main(String[] args) {
int age = 25; // Primitive -> Stored inside main's stack frame
String role = "Engineer"; // Reference 'role' in Stack -> Literal stored in Heap (String Pool)
User user = new User("Alice"); // Reference 'user' in Stack -> Object 'new User()' on Heap
calculate(age); // Pushes new stack frame for calculate()
}
private static void calculate(int value) {
int result = value * 2; // 'result' and 'value' live in calculate's stack frame
} // calculate's frame pops off; stack memory instantly reclaimed
}
Thread Stack (Private) JVM Heap (Shared)
┌─────────────────────────┐ ┌───────────────────────────────────────┐
│ Frame: calculate() │ │ │
│ int value = 25 │ │ ┌─────────────────────────────────┐ │
│ int result = 50 │ │ │ User Object │ │
├─────────────────────────┤ │ │ name: "Alice" │ │
│ Frame: main() │ │ └─────────────────────────────────┘ │
│ int age = 25 │ │ ▲ │
│ String role ───────────┼───────────────┼──> "Engineer" │ │
│ User user ─────────────┼───────────────┼───────────────────┘ │
└─────────────────────────┘ └───────────────────────────────────────┘
🟡 Middle Level
Stack Architecture: Anatomy of a Stack Frame
Whenever a Java thread invokes a method, the JVM pushes an isolated Stack Frame onto that thread’s call stack. The frame’s memory dimensions are pre-calculated at compile time by javac and embedded in the class bytecode:
┌────────────────────────────────────────────────────────┐
│ STACK FRAME │
├────────────────────────────────────────────────────────┤
│ 1. Local Variable Table (LVT) │
│ - Slot [0] = 'this' pointer (for instance methods) │
│ - Slots [1..N] = Method arguments and local vars │
│ - Long & Double occupy 2 contiguous 32-bit slots │
├────────────────────────────────────────────────────────┤
│ 2. Operand Stack │
│ - LIFO workspace for executing bytecode operations │
│ - e.g., 'iload_1', 'iload_2', 'iadd', 'istore_3' │
├────────────────────────────────────────────────────────┤
│ 3. Frame Data │
│ - Dynamic Linking (symbolic references to Pool) │
│ - Normal Method Completion (caller return PC) │
│ - Exception Dispatch Reference (handler table) │
└────────────────────────────────────────────────────────┘
Increasing -Xss allows deeper recursive calls but reduces the maximum number of concurrent threads that can be spawned within the process’s physical memory space.
Heap Allocation: TLAB and Bump-the-Pointer
In a multithreaded runtime, multiple threads allocating objects on a single shared Heap simultaneously would require mutex synchronization, causing massive thread contention.
To eliminate this bottleneck, the HotSpot JVM employs Thread Local Allocation Buffers (TLAB):
- When a thread starts, the JVM assigns it a dedicated, private slice of the Eden space (typically tens to hundreds of kilobytes).
- Inside its TLAB, a thread allocates memory using Bump-the-Pointer: it simply increments an allocation pointer by the byte size of the object (
pointer += size). - This allocation requires zero locks and zero atomic CAS operations, making object allocation in Java faster than native
malloc()in C++. - When a TLAB fills up, the thread uses an atomic CAS instruction to request a new TLAB chunk from Eden, or falls back to shared Eden allocation for massive objects.
JIT Optimization: Escape Analysis and Scalar Replacement
Under HotSpot JIT (C2 compiler), objects are not always allocated on the Heap. During compilation, the JIT performs Escape Analysis:
- GlobalEscape: The object reference escapes the current method and thread (e.g., returned via method, stored in a static variable, or assigned to a shared object field). Must reside on the Heap.
- ArgEscape: The reference is passed as an argument to another method but does not outlive the caller frame.
- NoEscape: The object is created, used, and discarded strictly within the scope of a single method.
public int calculateTotal() {
Point point = new Point(10, 20); // NoEscape: reference never leaves this method
return point.x + point.y;
}
When an object qualifies as NoEscape, the C2 compiler applies Scalar Replacement:
- The object structure is completely disassembled into its constituent primitive scalar variables (
int point_x = 10; int point_y = 20;). - The fields are placed directly into CPU registers or on the Stack frame.
- The physical object is never allocated on the Heap, generating zero Garbage Collection overhead.
- Lock Elision: If
synchronized(point)was called on an unescaped object, the synchronization monitor is completely stripped out by the JIT compiler.
🔴 Senior Level
Compressed OOPs and the 32 GB Memory Cliff
On 64-bit architectures, native memory pointers (Ordinary Object Pointers, or OOPs) occupy 8 bytes (64 bits). This increases heap consumption by 30–40% and degrades CPU L1/L2/L3 cache hit ratios.
To overcome this, HotSpot implements Compressed OOPs (-XX:+UseCompressedOops):
- All Java objects in memory are aligned to an 8-byte boundary (the lowest 3 bits of any object’s memory address are always
000). - The JVM discards these three zero bits when storing the reference, saving the address as a 32-bit integer.
- When loading a pointer into a CPU register, the CPU performs a bitwise left-shift by 3 (
address << 3) to restore the 64-bit physical address. - This allows a 32-bit integer to address up to: \(2^{32} \times 8 \text{ bytes} = 32 \text{ GB}\)
Heap < 32 GB: References consume 4 bytes (Compressed OOPs active)
Heap >= 32 GB: References consume 8 bytes (Compressed OOPs disabled!)
[!IMPORTANT] The 32 GB Highload Cliff: A JVM configured with
-Xmx31goften holds more usable payload data and performs 15–20% faster than a JVM configured with-Xmx33g. Crossing the 32 GB boundary forces pointers to expand from 4 bytes to 8 bytes, instantly consuming 2–4 GB of additional RAM just to store object references and causing severe CPU cache thrashing.
Object Memory Layout in Heap
Every object allocated on the 64-bit JVM Heap adheres to a structured memory footprint:
┌────────────────────────────────────────────────────────┐
│ OBJECT HEADER │
├────────────────────────────────────────────────────────┤
│ 1. Mark Word (8 bytes on 64-bit JVM) │
│ - Identity HashCode (lazily calculated) │
│ - Age bits (4 bits: tracks promotions 0–15) │
│ - Lock status bits (biased, lightweight, heavyweight)│
│ - GC metadata tagging bits │
├────────────────────────────────────────────────────────┤
│ 2. Klass Word (4 bytes with CompressedClassPointers) │
│ - Pointer to class metadata in Metaspace │
├────────────────────────────────────────────────────────┤
│ 3. Array Length (4 bytes, present ONLY in arrays) │
├────────────────────────────────────────────────────────┤
│ 4. Instance Data (Primitive fields & reference fields) │
│ - Compacted & reordered by JVM to minimize gaps │
├────────────────────────────────────────────────────────┤
│ 5. Padding (Alignment zeroes) │
│ - Padded with zeroes to round up to a multiple of 8 │
└────────────────────────────────────────────────────────┘
An empty new Object() occupies exactly 16 bytes of heap memory (8-byte Mark Word + 4-byte Klass Word + 4 bytes of padding).
NUMA (Non-Uniform Memory Access) Topology
In multi-socket production servers, RAM is partitioned among physical CPU sockets. Accessing local RAM attached to Socket 0 is 2–3x faster than accessing RAM wired to Socket 1 across the UPI/QPI interconnect.
- Passing
-XX:+UseNUMAinstructs the JVM to allocate G1/Parallel GC Eden regions with awareness of NUMA nodes. - A thread executing on Socket 0 receives its TLAB in Socket 0’s local memory, slashing inter-socket interconnect traffic and lowering allocation latency.
Project Loom: Virtual Threads Stack Architecture (Java 21+)
Classic platform threads (java.lang.Thread) map 1:1 to OS native threads (pthreads), each requiring a fixed, contiguous OS stack (default 1 MB). Launching 10,000 platform threads consumes 10 GB of physical RAM solely for stack allocations.
Virtual Threads Architecture:
- Virtual Threads do not hold dedicated 1 MB OS stacks.
- Their stack frames reside in the Heap as chunked
Continuationobjects. - When a virtual thread executes a blocking operation (e.g., JDBC query, socket read,
Thread.sleep()), it unmounts from its Carrier Thread. Its stack is frozen into the Heap, starting at only a few hundred bytes. - When unblocked, the continuation is mounted onto any idle Carrier Thread. This drops the per-thread memory footprint from 1 MB to ~1–2 KB, enabling millions of concurrent threads per JVM.
4 Tricky Questions
1. Can an object in Java be allocated on the Stack rather than the Heap?
Answer: Formally, the Java Virtual Machine Specification specifies that all object instances are created on the Heap. However, in modern JVM implementations (HotSpot C2), the compiler performs Escape Analysis. If an object does not escape the allocating method frame (NoEscape), the JVM applies Scalar Replacement: the object is not created as a unified heap entity; instead, its constituent fields are decomposed into standalone primitive variables and allocated directly into CPU registers or the thread’s Stack frame. While this is technically scalar replacement rather than standard object stack allocation, the operational effect is identical: zero heap allocation and zero GC overhead.
2. Why does a JVM configured with 31 GB of heap often provide more usable capacity and higher throughput than a 33 GB heap?
Answer: Up to 32 GB, the 64-bit JVM utilizes Compressed OOPs (-XX:+UseCompressedOops), compressing 64-bit memory addresses into 32-bit references by exploiting 8-byte object alignment. When the heap size reaches or exceeds 32 GB, Compressed OOPs are automatically disabled, expanding every object reference and array pointer from 4 bytes to 8 bytes. In typical enterprise applications where references comprise 20–30% of heap footprint, crossing 32 GB instantly consumes several gigabytes of RAM purely to store wider pointers. Consequently, a 33 GB heap often provides less usable payload capacity than a 31 GB heap, while simultaneously degrading CPU L1/L2/L3 cache hit ratios.
3. Where do Virtual Threads (Project Loom) store their call stacks in Java 21+, and how does this affect memory footprint?
Answer: Unlike platform threads that pre-allocate a fixed 1 MB contiguous memory block from OS virtual memory, Virtual Threads store their call stacks as chunked continuation frames inside the JVM Heap. When a Virtual Thread runs, its current frame is mounted onto the small native stack of an underlying OS Carrier Thread. When the Virtual Thread blocks on I/O, its stack frame is unmounted and preserved in the Heap. Because Virtual Thread stack frames grow and shrink dynamically—starting at as little as a few hundred bytes—a JVM can host millions of concurrent Virtual Threads without running out of native stack memory.
4. What happens when a thread attempts to allocate an object or array larger than its available TLAB?
Answer: If an allocation request exceeds the remaining space in a thread’s TLAB:
- If the object size is smaller than the maximum TLAB limit, the thread retires its current TLAB, wastes the remaining slack space (or fills it with a dummy
int[]filler object for GC parsers), and requests a fresh TLAB from Eden using an atomic CAS operation. - If the object size exceeds the configured TLAB ceiling, the thread bypasses TLAB allocation entirely and allocates the object directly into shared Eden space using atomic synchronization.
- In the G1 collector, if the allocation exceeds 50% of a G1 region size, it is classified as a Humongous Object and allocated into a contiguous sequence of dedicated Humongous Regions directly in the Old Generation.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“Stack is thread-private memory with fast LIFO access, storing method call frames, local primitives, and object references. It is deterministically reclaimed by CPU stack pointer adjustments on method return, and exhausting it causes
StackOverflowError. Heap is globally shared memory housing all object instances, arrays, and the String pool, managed by the Garbage Collector and subject toOutOfMemoryError. Heap allocations are optimized via lock-free TLABs. Furthermore, JIT Escape Analysis can eliminate heap allocation entirely via Scalar Replacement, mapping unescaped object fields to registers/stack slots. A critical production boundary is 32 GB, beyond which Compressed OOPs are disabled, bloating pointers and hurting performance.”
Key Architectural Takeaways
- Stack: Thread-isolated, fast, LIFO, stores frames and primitives; overflow $\to$
StackOverflowError. - Heap: Shared, stores objects/arrays, reclaimed by GC; overflow $\to$
OutOfMemoryError: Java heap space. - TLAB: Thread-local Eden buffer; enables lock-free bump-the-pointer object allocation faster than C
malloc(). - Escape Analysis: JIT optimization decomposing non-escaping objects into stack/register scalars.
- Compressed OOPs (32 GB Rule): Pointers compress to 4 bytes under 32 GB; exceeding 32 GB doubles pointer size to 8 bytes, degrading CPU cache utilization.
- Virtual Threads (Java 21): Stack frames are stored in the Heap as continuation objects, reducing footprint from 1 MB to ~1–2 KB per thread.
Red Flags & Anti-Patterns
- 🚩 Claiming “The Garbage Collector cleans both Stack and Heap”: Stack memory is reclaimed automatically by hardware CPU stack pointer movement.
- 🚩 Asserting “Every
newstatement allocates memory on the Heap”: Ignores Escape Analysis and JIT Scalar Replacement. - 🚩 Sizing production heaps to
-Xmx34gwithout benchmarking: Crossing the 32 GB boundary disables Compressed OOPs, losing usable memory and hurting cache lines. - 🚩 Believing primitive types always live on the Stack: Primitive instance fields inside an object (
new User()) reside on the Heap within that object’s memory payload.
Related Topics
- What is stored in Heap — Object layout, mark word, and heap regions
- What is stored in Stack — Frame mechanics, local variable arrays, operand stack
- What is Garbage Collection — Tracing collectors and GC roots
- What are generations in GC — Eden, Survivor, Tenured, and Metaspace
- What are -Xms and -Xmx parameters — Heap sizing best practices