Notes on how stack and heap work across languages
For those curious about a detailed stack vs heap comparison in Rust, you can find a dedicated deep dive here.
TL;DR
Stack: Small, fast, per-thread memory for fixed-size, short-lived data. Ideal for locals and parameters. Limited size, overflow aborts or errors.
Heap: Large, flexible, process-wide memory for dynamic-size or long-lived data. Slower allocation, possible fragmentation, garbage-collected or manually managed.
Language differences:
- Rust: Deterministic destruction (
Drop
), you choose stack vs heap. - Go: Escape analysis decides; garbage-collected.
- Python: Almost everything on heap; refcount + GC.
- JavaScript: Almost everything on heap; GC with generational optimizations.
Deep dive: Detailed comparison and insights
Memory management model
- Rust: Manual allocation choice; deterministic cleanup with ownership model; allocator is pluggable; default is system allocator.
- Go: Runtime decides stack vs heap via escape analysis; GC reclaims heap memory; programmer can influence via code structure.
- Python: Objects always heap-allocated; CPython uses reference counting for prompt cleanup and a cyclic GC for cycles.
- JavaScript: Almost all values on heap; modern engines use generational, incremental, and concurrent garbage collectors.
Stack characteristics
- Rust: Fixed-size per thread, guard page prevents silent overflow; overflow aborts; drop timing follows scope.
- Go: Goroutine stacks start small (~2KB) and grow; frames may be moved during growth; pointer escapes trigger heap allocation.
- Python: Locals on C stack are just references; actual objects on heap; recursion depth limit enforced.
- JavaScript: Call stack bounded by engine; deep recursion throws
RangeError
.
Heap allocation and decision factors
- Rust: Explicit types like
Box
,Vec
,String
; large or dynamically sized values go here. - Go: Escape analysis automatically promotes values to heap if needed; no manual override.
- Python: All objects use heap;
pymalloc
optimizes small object allocation via arenas. - JavaScript: GC heap split into young/old generations; object lifetime influences where it lives.
Out-of-memory handling
- Rust: Default
handle_alloc_error
panics; recoverable allocation possible viatry_reserve
. - Go: Fatal runtime error; process terminates.
- Python: Raises
MemoryError
; recovery possible but rare in practice. - JavaScript: Process exits or tab crashes; no standard recovery.
Performance optimization patterns
- Rust: Pre-sizing containers, arena allocators,
SmallVec
to avoid heap; avoid trait-object boxing in hot paths. - Go: Use
make
with capacity, minimize pointer escapes, reuse buffers viasync.Pool
. - Python: Reuse buffers (
bytearray
,memoryview
), prefer vectorized libraries, avoid unnecessary temporaries. - JavaScript: Keep object shapes stable, prefer typed arrays for numeric data, reuse arrays and objects.
Common pitfalls and risks
- Rust: Stack overflow from deep recursion or large arrays; accidental heap allocations via trait objects.
- Go: Hidden heap escapes from closures or interface conversions.
- Python: Memory overhead per object; reference cycles if not broken.
- JavaScript: Shape changes hurting JIT optimizations; memory leaks from closures or global references.
Tooling to inspect and debug
- Rust:
perf
,heaptrack
,cargo flamegraph
,valgrind
. - Go:
pprof
profiles, escape analysis (-gcflags="-m"
). - Python:
tracemalloc
,objgraph
,memory_profiler
. - JavaScript: Chrome DevTools memory profiles, Node’s
--inspect
and heap snapshots.