Memorystackheapcall frameallocatorlifetime

Stack vs Heap

The stack is a bump pointer that allocates a function’s locals in one instruction and frees them on return; the heap is an allocator you ask for memory whose lifetime is not tied to any call — and every language you use maps its values onto those two regions differently.

ConceptualC++Node.jsCPython
▶ InteractiveInterview question
Progress

The problem

A function needs space for its locals for exactly as long as it runs; an object created in one function and returned to another needs space for as long as anyone holds it. One allocation rule cannot serve both. What are the two rules, and what does each cost?

Two regions, two lifetime rules

Inside a process’s address space (The Process Memory Layout) two regions grow during execution. The stack grows and shrinks with function calls: entering a function reserves space for its locals, returning releases it, and because calls nest perfectly the region is strictly last-in-first-out — the Stack from DSA, implemented by the hardware. The heap holds everything whose lifetime is *not* nested: a node inserted into a tree that outlives the function that built it, a buffer whose size is only known at runtime, an object shared between threads.

The two rules differ in who decides when memory is free. On the stack the answer is structural: the function returned, so its frame is gone, no bookkeeping required. On the heap someone has to decide — the programmer with free/delete, a reference count, or a garbage collector — and something has to remember which bytes are in use, which is what an allocator is.

One process, two growing regions
Stack (grows down): frames of main → handle → parseunmapped gap (guard)mmap region: shared libs, large allocationsHeap (grows up): objects, buffers, nodesdata / bss: globalstext: code
UserLLMAgentToolDataDecisionHumanGuardrail

The stack: bump a pointer

The CPU has a stack pointer register (rsp on x86-64, sp on ARM64). A function that needs 48 bytes of locals executes sub rsp, 48 on entry and add rsp, 48 before returning. That is the entire allocator: one instruction, no search, no metadata, no lock, ~1 ns. Locals are addressed as offsets from the stack pointer (or from a frame pointer, see Stack Frames), so the compiler resolves every access at compile time. The region is contiguous, hot in L1, and freed in the same order it was allocated.

The price is rigidity. A frame’s size must be known when the function is compiled (variable-length arrays and alloca exist but are frowned on), the memory is gone the moment the function returns — returning a pointer to a local is the classic use-after-return bug — and the whole stack has a fixed limit, typically 8 MB on Linux’s main thread and 1 MB on Windows (Stack Overflow). The stack is for data whose lifetime ends with the call and whose size is modest.

Stack vs heap in C++: the distinction is explicit in the source
1struct Node { int value; Node* next; };
2
3Node* build(int n) {
4 int count = 0; // stack: 4 bytes in build's frame, gone on return
5 Node* head = nullptr; // stack: the pointer itself
6 for (int i = 0; i < n; i++) {
7 head = new Node{i, head}; // heap: the Node outlives this call
8 count++;
9 }
10 return head; // returning the heap address is fine; returning &count would not be
11}

The heap: ask an allocator

malloc(48) cannot bump a pointer, because the caller may free that block in any order relative to every other block. The allocator keeps free lists — blocks of memory it currently owns but nobody uses — usually one list per size class (16, 32, 48, 64 … bytes) so that a request is served by popping the right list in O(1) without searching. When a list is empty the allocator carves a new chunk from a larger region (an arena) it obtained from the OS; when arenas are exhausted it asks the kernel for more address space (What Happens When I Allocate Memory?). Multi-threaded allocators keep per-thread caches so that two threads allocating at once do not contend on one lock.

The cost is 20–100 ns for a small allocation on a good allocator, and unbounded in the worst case: a size-class miss, an arena refill, a syscall, a page fault. The other cost is fragmentation: after a million allocations and frees of mixed sizes, free memory is scattered in pieces too small to satisfy a large request even though their sum would; the process’s footprint grows without its live data growing. Allocators fight this with size classes (internal fragmentation is bounded), coalescing adjacent free blocks, and returning whole empty arenas to the OS.

Heap memory must be freed by someone. In C and C++ that someone is you (free, delete, or RAII wrappers and smart pointers that do it deterministically). In garbage-collected runtimes it is the collector, which traces reachability from the roots — the stack is one of them — and reclaims what it cannot reach. In CPython it is mostly reference counting with a cycle collector for the rest. Every one of those strategies is a way to decide the one thing the stack never has to decide: is this still needed?

Stack and heap compared
PropertyStackHeap
AllocationMove the stack pointer: one instructionAllocator: free lists, size classes, maybe a syscall
DeallocationAutomatic on returnExplicit, refcount, or garbage collector
Cost~1 ns~20–100 ns typical; unbounded worst case
LifetimeNested in the callArbitrary
SizeFixed at compile time; total limit ~1–8 MBRuntime-sized; limited by address space and RAM
OrderLIFOAny
Failure modeOverflow (crash), use-after-returnLeak, use-after-free, fragmentation, double free
Thread-safetyPer-thread by constructionShared; allocator must lock or use per-thread caches

Where your language puts things

Runtime-specific

C++ makes the choice visible in the syntax: a local object (Node n;) lives in the frame; new Node lives on the heap; a std::vector<int> v has its 24-byte header on the stack and its elements on the heap; std::string keeps short strings (≤ 15 or 22 bytes, implementation-dependent) inline in the stack object and longer ones on the heap. Compilers will also keep an object in registers and never materialise it at all.

JavaScript/TypeScript (V8, in Node and Chromium) puts every object, array, closure and string on the managed heap; the stack holds only the engine’s frames and unboxed small integers (Smis) and pointers. The optimising compiler performs escape analysis: an object that provably never leaves the function — a temporary {x, y} used and discarded — can be kept in registers or scalar-replaced, so "everything is on the heap" is the semantic rule, not always the machine-level truth. When a function is deoptimised, those objects are materialised on the heap again.

Python (CPython) goes furthest: an int, a float, a tuple, a function’s frame — every value is a PyObject on the heap, and a local variable is a slot in a heap-allocated frame that holds a pointer to it. Small integers (−5…256) and interned strings are preallocated and shared; a for i in range(10**7) loop allocates and frees millions of int objects through pymalloc. This is why CPython arithmetic is slow and why x = 1 costs a refcount increment rather than a store. The C stack is used only by the interpreter itself.

  • C++: the source says where a value lives; the compiler may promote to registers.
  • JS/TS: semantically heap-everything; escape analysis in TurboFan/Maglev keeps non-escaping temporaries off the heap.
  • Python: everything is a heap object, including frames; refcounting frees most of it immediately.
  • Go and Java: escape analysis decides stack vs heap per allocation site; Go’s stacks are growable, so recursion depth is not the limit it is elsewhere.

Key points

  • Stack: allocated by moving the stack pointer, freed on return, LIFO, ~1 ns, fixed size per frame and a hard total limit.
  • Heap: allocated by an allocator with free lists and size classes, freed explicitly or by a GC, any lifetime, ~20–100 ns and unbounded worst case.
  • The stack never has to decide whether memory is still needed; the heap always does — that is the whole difference.
  • Fragmentation makes a process grow without its live data growing; allocators bound it with size classes and coalescing.
  • C++ exposes the choice in syntax; V8 puts objects on the heap unless escape analysis proves they need not be; CPython heap-allocates everything, even integers and frames.
  • Returning a pointer to a stack local is a use-after-return; returning a heap pointer is the normal way to hand data up the call chain.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why not put everything on the heap?

Because then every local would cost an allocator call and a deallocation decision. The stack exploits the fact that call lifetimes nest to make allocation and freeing free.

Why not put everything on the stack?

A stack frame dies when its function returns. Anything that must survive the call — a returned object, a node in a shared structure — needs a region whose lifetime is independent of the call chain.

Why is heap allocation slower?

It has to find a free block of the right size among blocks freed in arbitrary order, keep metadata to free it later, and stay consistent across threads. A stack allocation has none of those problems.

Why does Python allocate for 1 + 1?

Because every Python value is a heap object with a header, a type pointer and a refcount, and the result is a new object. Small integers are cached, but the machinery is still object machinery.

Stack and heap

Stack and heap, line by line
The same eight lines in three runtimes. Watch what each line does to the two regions.
C++Educational model
int main() {  int x = 5;  auto v = new Vec();  f(x);    int y = a * 2;  }  // f returns  delete v;}
Stack · grows ↓
(empty)
Heap · 0 B live
(nothing allocated)
Locals and by-value arguments are bytes in the frame; only what you `new` lives on the heap, and you free it yourself.
1/9 · before main

How it fails

What the failure looks like from inside real software.

  • Use-after-return in C/C++: a function returns &local; the caller reads garbage or a later frame — ASan reports stack-use-after-return.
  • Heap fragmentation in a long-running service: RSS grows for days while live data is flat; jemalloc/tcmalloc or an arena-per-request design fixes it.
  • A large array declared as a local in C++ (int buf[4000000]) overflows the 8 MB stack on the first call.
  • A JavaScript hot loop allocating a small object per iteration: young-generation GC pauses every few ms; the fix is to reuse the object or let escape analysis see it does not escape.
  • Python code building millions of tiny objects: memory 5–10× the raw data size because each object carries a 16–28 byte header; array, NumPy or __slots__ shrink it.