Stack vs Heap

7 min read

Reading Progress0%
CS Fundamentals Index
Tier 1 -- Foundations
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs
CS Fundamentals Index
Tier 1 -- Foundations
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs

Stack vs Heap

1. What Is It?

When your program runs, its memory is split into regions with different rules. Two matter most: the and the .

  • The holds function call frames — local variables, parameters, return addresses. It grows as functions are called and shrinks as they return. Allocation is a single pointer bump; deallocation happens automatically when the function exits.
  • The holds data whose lifetime isn't tied to a single function call — objects that need to outlive the function that created them, or whose size isn't known at compile time. Allocation is more expensive, and cleanup is either manual (C/C++) or handled by a garbage collector (Python, Java).

Without this split, the language couldn't cheaply reuse memory across function calls (stack), nor let data live past the function that created it (heap).

QUICK CHECK

A web server handles requests by spawning a function that builds a response object. The response object needs to be passed to a logging service and a caching layer after the original request-handling function has already returned. Where should this response object be allocated, and why?

Choose one answer

2. How It Works

The stack: last in, first out

Every function call pushes a frame onto the . The frame holds:

  • Parameters
  • Local variables
  • The return address (where to resume in the caller)
  • Saved registers

When the function returns, its frame is popped — the stack pointer moves back up, and that memory is instantly reusable. This is why stack allocation is essentially free: a single instruction adjusts one register.

The heap: long-lived, manually managed

The is a large memory pool the program requests chunks from. In C++, new and malloc ask for memory; you must later delete or free it. In Python and Java, the language runtime allocates on the heap for you and a garbage collector reclaims unreachable memory.

// C++ — stack and heap side by side
void f() {
    int localValue = 42;                  // on the stack, lives until f() returns
    int* heapValue = new int(42);         // on the heap, lives until 'delete'
    delete heapValue;                     // must free manually
}   // localValue is reclaimed automatically; heap leak if delete was forgotten
// Java — primitives on stack (as frame locals), objects always on heap
void f() {
    int localValue = 42;                  // in the stack frame
    Point point = new Point(1, 2);        // the Point object is on the heap;
                                          // 'point' (a reference) is in the stack frame
}   // frame pops; Point is collectible when no references remain
# Python — integers, strings, everything is a heap object;
# names (variables) are references stored in the frame
def f():
    localValue = 42                       # name 'localValue' in the frame
                                          # but the int object '42' is on the heap
    nums = [1, 2, 3]                      # list object on the heap

What lives where

ThingPythonJavaC++
Primitive-ish locals (int, double)names on frame, values heap-allocatedon the frameon the frame
Objectsalways heapalways heapstack or heap (your choice)
Function parameterson the frame (as references)on the frameon the frame (copies or references)
new-allocatedn/aheapheap (you manage)

Why objects default to heap in Python and Java

Objects can outlive the function that created them — you might return one, put it in a list, or hand it to another thread. A stack frame only exists while the function runs. If you let someone hold a pointer into a popped frame, you get a dangling pointer. Python and Java side-step this entirely: every lives on the heap, so references stay valid as long as any code still holds one.

C++ lets you put objects on the stack anyway, because it's faster and the compiler can enforce lifetime via . But returning a pointer to a stack local is a classic bug:

// C++ — DO NOT DO THIS
int* buggy() {
    int localValue = 42;
    return &localValue;          // dangling: frame is gone as soon as we return
}
QUICK CHECK

A backend developer writes a C++ function that builds a configuration object and returns a pointer to it. During code review, a senior engineer flags this as a critical bug:

Config* getConfig() {
    Config cfg;         // created here
    cfg.timeout = 30;
    return &cfg;        // returned to caller
}
Why is this code dangerous?

Choose one answer

3. What You Actually Need to Know

  • memory is tiny. Typical defaults: 8 MB for the main thread on Linux and macOS (check with ulimit -s); secondary threads are smaller — 512 KB on macOS, around 8 MB on Linux glibc (set via pthread_attr_setstacksize). JVM threads default to ~512 KB–1 MB (-Xss). Huge arrays or deep overflow it — you get a StackOverflowError (Java), RecursionError (Python), or a segfault (C++).
  • memory is large but slow-ish. Allocation involves bookkeeping (finding a free block, updating free lists). For hot paths, allocating many small objects on the is a measurable cost.
  • allocation is automatic; heap isn't. In C++, every new needs a matching delete. In Python/Java, the GC handles it — but "unreachable" is what matters, and holding onto references (e.g., a cache, a static list) keeps objects alive longer than you'd expect.
  • Size must be known at compile time to go on the stack (in C++). Runtime-sized arrays go on the heap. Python and Java don't really give you the choice — objects always go on the heap.
  • Debugging clues:
    • StackOverflowError / RecursionError / segfault in a recursive function → runaway . Missing base case, or the input was too deep. Convert to iteration or increase stack size.
    • Accessing freed memory (C++) → classic "use after free". Tools: AddressSanitizer, Valgrind.
    • growing over time (C++) → a new without delete, or a cycle your ref-counted smart pointers can't break.
    • High memory usage that doesn't go down (Java/Python) → something still holds references. Look for caches, static collections, listener lists.
QUICK CHECK

A Java backend service processes millions of requests per day and runs fine initially, but its memory usage climbs steadily over hours until it is eventually killed by the OS. Heap dumps show that most memory is held by a static List that collects event objects added during request handling but never removed. What is the most accurate description of what is happening?

Choose one answer

4. Language Differences

  • C++: you pick or per . objects are destroyed deterministically when their ends (enables RAII — resource cleanup tied to lifetime). objects need delete or a smart pointer (std::unique_ptr, std::shared_ptr).
  • Java: primitives on the stack frame, objects always on the heap, GC reclaims them. JIT can sometimes "escape-analyze" short-lived objects onto the stack as an optimization, but from your code's perspective every object is heap.
  • Python: everything is a heap object, including small ints and strings (which are often cached/interned). counting + cyclic GC handles cleanup. Names in the local frame are just references to heap objects.

Go, Rust, and C# do similar things for similar reasons. Go adds escape analysis at compile time — if an object provably can't outlive its stack frame, it stays on the stack; otherwise it escapes to the heap.

QUICK CHECK

A Go developer writes a function that creates a small configuration struct, uses it only within that function, and returns a computed result (not the struct itself). At compile time, Go's escape analysis determines the struct cannot outlive the function's stack frame. Where will Go most likely allocate this struct, and why?

Choose one answer

5. Tradeoffs & Decisions

  • Allocate on the when you can. Faster, no GC pressure, automatic cleanup. In C++ this is a routine choice; in Python/Java the language chooses for you.
  • Use the when the must outlive its creating function, when its size is only known at runtime, or when it's too big for the (large buffers, images).
  • Deep → iteration or explicit stack. If depth depends on input, it will overflow. Convert to a loop with a -allocated stack structure (list, ArrayDeque, std::stack).
  • Many small allocations → pool or arena. In hot paths, allocating millions of tiny objects on the heap thrashes the allocator and the GC. Reuse buffers, preallocate, or pool.
  • In C++, prefer value types and unique_ptr over raw new. Raw new and delete invite leaks, double frees, and dangling pointers. Smart pointers tie heap lifetime to .

If you see allocation profiling dominated by a small struct, move it to the stack or reuse a buffer. If you see a StackOverflowError, either bound the recursion or flip to iteration with an explicit heap-allocated stack.

QUICK CHECK

A backend service processes millions of small event objects per second in a hot path. Profiling reveals the heap allocator is being thrashed and GC pauses are spiking. Which approach best addresses this bottleneck?

Choose one answer

6. Interview Cheat Sheet

  • The holds function frames (locals, parameters, return addresses). Allocation is a pointer bump; deallocation is automatic on return. Fast but small and -bound.
  • The holds long-lived data — anything that must outlive the function that created it, or is too big / runtime-sized. Slower to allocate; cleanup is manual (C/C++) or via GC (Python/Java).
  • Java/Python objects always live on the ; primitives and references live on the frame.
  • C++ lets you choosestack for speed and deterministic lifetime, heap via new / smart pointers for long-lived or large data.
  • Classic bugs: stack overflow (runaway ), dangling pointer (keeping a pointer to a popped frame in C++), (forgotten delete or lingering in GC languages).

Follow-ups:

  • "Why can't you just put everything on the heap?"Heap allocation is slower and fragments memory. Stack allocation is nearly free and cleans up automatically. Languages default to heap only where they must ( lifetimes outliving frames).
  • "What causes a stack overflow?" — Usually unbounded , occasionally a massive local (e.g., int huge[10_000_000] in C++). Each call consumes a frame; the stack is small.
  • "How do Java/Python avoid dangling pointers?" — They allocate objects on the heap and rely on GC. As long as any exists, the stays alive; once no references remain, the GC reclaims it.
  • "What is RAII?" — Resource Acquisition Is Initialization: in C++, tie a resource (memory, file, lock) to the lifetime of a stack object. The destructor runs deterministically on exit and releases the resource.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.