Every running program splits its memory into two big regions. The stack is fast, ordered, and tied to the call you're inside right now. The heap is for things that need to outlive the function that made them. Most "weird" bugs — leaks, dangling pointers, surprise nulls — are really arguments about which region a value belongs in.
free, or the end of the program — releases them.Grows downward. One per thread. Small (typically 1–8 MB). Recursion-bombs blow it up — that's a StackOverflow.
Grows upward. Shared across threads. Effectively unlimited (until the OS says no). Where new, malloc, and most objects live.
Globals, string literals, the program's instructions. Loaded once, never resized.
Memory-mapped files, shared libraries, kernel pages. Visible to your process but managed by the OS.
Languages with managed runtimes (JVM, .NET, Go, JS) add their own substructure on top — generations, eden space, large-object heap — but the stack/heap split is the foundation underneath all of it.
Every call pushes a frame: arguments, local variables, the return address, and saved registers. Return pops it. This is why deep recursion crashes — there's a hard ceiling, and unlike the heap it doesn't grow on demand.
Integers, floats, booleans, fixed-size structs. In C++/Rust/C#/Go, structs default to stack allocation when the compiler can prove they don't escape the function. In Java/Python/JS, primitive locals sit on the stack but objects always go on the heap (with escape-analysis exceptions on the JVM).
Allocation is "subtract from the stack pointer." Deallocation is "add back." No bookkeeping, no fragmentation, no GC. Cache-friendly: the top of the stack is almost certainly hot in L1.
A value returned from a function, an object held by a long-lived service, a cache entry, a socket buffer. Anything whose lifetime crosses a function boundary has to live somewhere stable — that's the heap.
A list whose length is read from a file, a string built at runtime, a tree of arbitrary depth. The compiler can't reserve a fixed slot, so it asks the allocator for a block.
| Language | Memory Model | Who Frees |
|---|---|---|
| C / C++ | Manual. Stack for locals, new/malloc for heap. RAII (C++) ties heap lifetime to stack scope via destructors. | You. |
| Rust | Ownership + borrow checker. Heap via Box, Vec, Rc. Compiler proves lifetimes; no GC. | Compiler-inserted drops. |
| Java / C# / Kotlin | Almost everything is on the heap. Generational GC, escape analysis can stack-allocate hot objects. | Garbage collector. |
| Go | Stack by default, escape analysis promotes to heap. Concurrent low-latency GC. | Garbage collector. |
| Python / JS / Ruby | Objects on the heap. Reference counting (CPython) or tracing GC (V8, MRI's mark-sweep). | Runtime. |
| Swift / Obj-C | Automatic Reference Counting (ARC) — compiler inserts retain/release. | ARC + you (cycles). |
The cheapest heap allocation is the one you skip. Pool buffers, reuse slices, prefer arrays of structs over arrays of pointers. Profile allocations the way you profile CPU — they're often the hidden cost in "GC pause" stories.
An array of 1M ints walks the cache linearly. A linked list of 1M ints chases a pointer per element and stalls the CPU. Same Big-O, different real-world performance by 10–100×.
JVM, Go, .NET, V8 all have tunables and tradeoffs (throughput vs latency, generational vs region-based). For latency-sensitive services, learn what your GC does on a major collection — then either reduce allocation, switch collector, or accept the pause.