Homeβ€Ί DSAβ€Ί Stack vs Heap Memory: What Every Developer Must Know

Stack vs Heap Memory: What Every Developer Must Know

Where developers are forged. Β· Structured learning Β· Free forever.
πŸ“ Part of: Stack & Queue β†’ Topic 9 of 10
Understand the difference between stack and heap memory in Java.
βš™οΈ Intermediate β€” basic DSA knowledge assumed
In this tutorial, you'll learn:
  • Stack holds method frames (local variables, parameters, return addresses) and is automatically reclaimed when a method returns. Heap holds objects and is managed by the GC.
  • Stack memory is thread-private β€” each thread has its own stack. Heap memory is shared across all threads, which is why heap objects need synchronisation.
  • StackOverflowError = stack full from unbounded recursion. OutOfMemoryError = heap full from too many live objects.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
⚑ Quick Answer
The stack is a notepad for a method β€” variables appear when the method starts and vanish the moment it returns. The heap is a shared workspace where objects live until nobody needs them. Understanding which memory region holds what determines how you reason about object lifetime, threading safety, and GC pressure.

Stack vs heap is one of those fundamentals that separates developers who write code from developers who understand what their code does at runtime. I've diagnosed StackOverflowErrors from unbounded recursion in production and tuned JVM heap flags for services handling 50,000 requests per minute. Both incidents would have been faster to resolve if the engineers involved had a clear mental model of memory allocation.

How Stack Memory Works

Every thread has its own call stack. When a method is called, the JVM pushes a new stack frame containing: the method's local variables and parameters, the method's return address, and intermediate computation results. When the method returns, the frame is popped and the memory is instantly reclaimed β€” no garbage collection, no overhead.

Stack allocation is O(1) β€” incrementing a pointer. That's why local primitives are fast. The downside: the stack is small (256KB–1MB per thread by default). Deep or unbounded recursion fills it and throws StackOverflowError.

StackMemoryDemo.java Β· JAVA
1234567891011121314151617181920212223242526272829
package io.thecodeforge.memory;

public class StackMemoryDemo {

    public static void main(String[] args) {
        // 'multiplier' lives on main()'s stack frame
        int multiplier = 3;
        // A new stack frame is created for calculate(5)
        int result = calculate(5);
        System.out.println(result * multiplier); // 45
    } // main() stack frame popped β€” 'multiplier' and 'result' gone

    static int calculate(int input) {
        int doubled = input * 2;    // on calculate()'s stack frame
        int added   = doubled + 25; // also on this frame
        return added;
    } // Frame popped β€” 'doubled' and 'added' are gone immediately

    // StackOverflowError: no base case, frames accumulate indefinitely
    static void unbounded(int n) {
        unbounded(n + 1);
    }

    // Safe: bounded recursion with a base case
    static int factorial(int n) {
        if (n <= 1) return 1;          // Base case β€” stops recursion
        return n * factorial(n - 1);   // At most n frames on stack
    }
}
β–Ά Output
45

// unbounded() produces:
// Exception in thread 'main' java.lang.StackOverflowError
// at io.thecodeforge.memory.StackMemoryDemo.unbounded(StackMemoryDemo.java:16)
// at io.thecodeforge.memory.StackMemoryDemo.unbounded(StackMemoryDemo.java:16)
// ... (thousands more identical frames)

How Heap Memory Works

The heap is where objects live. new PaymentService() allocates the object on the heap. The reference (a 4-8 byte pointer) to that object lives on the stack (or inside another object on the heap). When no references point to an object, it becomes eligible for garbage collection.

The JVM heap is divided into generations. Young generation holds newly created objects β€” minor GC runs frequently here and is fast. Objects that survive several minor GCs are promoted to Old generation β€” collected less often, in a major GC that can pause the application. Creating millions of short-lived objects in a hot path causes 'GC pressure' β€” frequent minor GCs that add latency spikes even when total memory use seems fine.

Configure heap size with JVM flags: -Xms for initial size, -Xmx for maximum. Each thread's stack size is configured with -Xss.

HeapMemoryDemo.java Β· JAVA
123456789101112131415161718192021222324252627282930313233343536
package io.thecodeforge.memory;

public class HeapMemoryDemo {

    // Object fields live on the heap (part of the object)
    static class PaymentRecord {
        String    paymentId;    // reference on heap; String object also on heap
        int       amountPence;  // primitive stored directly in the object on heap
        boolean   processed;
    }

    public static void main(String[] args) {
        // 'record' reference: on main()'s stack frame (4-8 bytes)
        // PaymentRecord object: on the heap (size = fields + object header ~16 bytes)
        PaymentRecord record = new PaymentRecord();
        record.paymentId   = "pay-42";
        record.amountPence = 10_000;   // Β£100.00
        record.processed   = false;

        process(record);  // passes the reference β€” NOT a copy of the object

        record = null;    // No more references β†’ object eligible for GC
    } // 'record' reference popped from stack; object waits for GC

    static void process(PaymentRecord r) {
        // 'r' is a copy of the reference on THIS frame's stack
        // r.processed = true modifies the SAME object on the heap
        r.processed = true;
        System.out.println("Processed: " + r.paymentId);
    } // 'r' reference gone; heap object still exists (main() still holds it)

    // JVM flags for memory tuning:
    // java -Xms512m -Xmx2g -Xss512k -jar app.jar
    //      ^^^^^^^^  ^^^^   ^^^^^^^  
    //      init heap  max   stack per thread
}
β–Ά Output
Processed: pay-42

// Memory layout while process() runs:
// Stack (main frame): record β†’ 0x1a2b3c4d
// Stack (process frame): r β†’ 0x1a2b3c4d (same address)
// Heap (0x1a2b3c4d): PaymentRecord{paymentId='pay-42', amountPence=10000, processed=true}
CharacteristicStackHeap
StoresLocal vars, params, referencesObjects, instance fields, arrays
Managed byJVM (LIFO, automatic)Garbage Collector
Size256KB–1MB per thread (default)Configurable with -Xmx (GBs)
SpeedVery fast (pointer increment)Slower (allocation + GC overhead)
LifetimeMethod execution scopeUntil no references remain
Error when fullStackOverflowErrorOutOfMemoryError
Thread accessPrivate per threadShared by all threads
GC involved?NoYes

🎯 Key Takeaways

  • Stack holds method frames (local variables, parameters, return addresses) and is automatically reclaimed when a method returns. Heap holds objects and is managed by the GC.
  • Stack memory is thread-private β€” each thread has its own stack. Heap memory is shared across all threads, which is why heap objects need synchronisation.
  • StackOverflowError = stack full from unbounded recursion. OutOfMemoryError = heap full from too many live objects.
  • Passing an object to a method passes the reference (cheap pointer copy). The method sees the same heap object and can mutate its fields.
  • JVM flags: -Xms (initial heap), -Xmx (max heap), -Xss (stack size per thread). Tune these based on your application's object allocation profile.

⚠ Common Mistakes to Avoid

  • βœ•Assuming primitives always live on the stack β€” primitive fields of an object live on the heap as part of that object. Only local primitive variables in method bodies live on the stack.
  • βœ•Confusing passing a reference with copying an object β€” when you pass an object to a method, you pass the reference (4-8 bytes). The method can modify the object's fields but cannot change what the caller's variable points to.
  • βœ•Not understanding that stack is thread-private β€” each thread has its own stack. Race conditions can only occur on heap objects accessed by multiple threads, not on local stack variables.
  • βœ•Creating large temporary objects in hot loops β€” each allocation goes to Young generation. Frequent minor GC cycles from unnecessary allocations cause latency spikes even when memory seems plentiful.

Interview Questions on This Topic

  • QWhat is the difference between stack and heap memory? Where does a Java object live?
  • QA method creates a List<String> with 10,000 elements and passes it to another method. What happens in memory?
  • QWhy is stack memory thread-safe while heap memory is not?
  • QWhat causes StackOverflowError vs OutOfMemoryError?

Frequently Asked Questions

What is the difference between stack and heap memory in Java?

The stack stores method frames containing local variables, parameters, and return addresses. It's thread-private and automatically reclaimed when a method returns. The heap stores objects created with 'new' and is managed by the garbage collector. Objects live on the heap until no references point to them.

Where do primitive variables live in Java?

It depends on context. Primitive local variables and method parameters live on the stack. Primitive fields of an object live on the heap as part of that object's memory block.

What causes StackOverflowError?

Unbounded recursion β€” a method that calls itself without a base case that terminates. Each call adds a frame to the stack. When the stack exhausts its allocated space (typically 256KB–1MB per thread), the JVM throws StackOverflowError.

πŸ”₯
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousImplement Stack using QueueNext β†’Monotonic Stack: Pattern, Use Cases and Coding Examples
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged