Memory Management
Stack, heap, GC, RAII, borrow checker — and how each one fails.
Memory Management
Every running program has a stack and a heap. The stack is a simple LIFO of call frames — each function push/pop is a couple of instructions. The heap is a general-purpose pool managed by an allocator (malloc, mmap, V8's spaces, the JVM's generations). The interesting question is: who is responsible for giving heap memory back?
Analogy
Think of the cutlery drawer at a big restaurant. The stack is the clean knives and forks bussed out at the start of each table — grab one when seated, drop it back when the table leaves; order is perfectly predictable. The heap is the enormous ever-rotating pile of loaner props in the kitchen: anyone can grab a serving platter whenever they like, for as long as they like. The only question is how that platter gets returned — the chef washes it themselves (manual), a dishwasher is hired full-time to circle the tables looking for idle plates (garbage collection), or the restaurant policy marks each platter with the table that "owns" it and it goes back the moment that table leaves (ownership).
Manual management
C and C++ (when you avoid smart pointers) require the programmer to call free for every malloc. Three bugs fall out of that contract:
- Leak: you never call
free. Heap grows until the OS kills the process. - Double-free: you call
free(p)twice. The allocator's free-list is corrupted. - Use-after-free: you dereference
pafterfree(p). Those bytes may now belong to a different allocation — often exploitable.
char *buf = malloc(256);
strcpy(buf, input);
// ... forget to call free(buf) ...
Manual memory is fast and predictable — no GC pauses, no hidden costs — but the bugs are catastrophic and undebuggable without sanitizers (ASan, Valgrind).
RAII: destructors tied to scope
C++ invented Resource Acquisition Is Initialization: objects hold resources, and the language guarantees the destructor runs when the object leaves scope. std::vector, std::unique_ptr, std::lock_guard — all RAII.
{
std::vector<int> v(1000); // heap allocation happens
// ...use v...
} // v's destructor runs here; heap freed.
You never write delete. The compiler inserts the calls because the language knows where the scope ends. This is memory-safe as long as you don't hold raw pointers to the managed memory after the owner dies.
Rust's borrow checker
Rust takes RAII and adds a compile-time proof. Every value has a single owner; when the owner goes out of scope, rustc inserts a drop call. The borrow checker enforces two rules:
- A value can be moved to a new owner, after which the old owner cannot be used.
- At any point, a value has either one mutable reference or any number of immutable references — never both.
let s = String::from("hi");
let t = s; // ownership moved into t
println!("{}", s); // error: value borrowed after move
The same rules rule out use-after-free, double-free, and data races at compile time. There is no GC; no runtime cost.
Garbage collection
Java, Go, C#, OCaml, Haskell, Python, Ruby, and JavaScript all use GC — but the flavours vary.
Tracing GC (HotSpot G1/ZGC, Go's GC, V8's GC) periodically walks from the roots (stacks, globals, registers) to all reachable objects. Everything else is unreachable and gets reclaimed. This is what most people mean by "GC."
Reference counting (CPython's primary mechanism) increments a counter on each reference and decrements it on each drop; when the count hits zero, the object is freed immediately. Cycles need a separate collector.
GC languages can still leak! If you keep a reference to an object in a module-level cache, it stays reachable, and the collector can't touch it. The JS classic is a closure that keeps hold of a big object forever.
Stack vs heap allocation
The stack is free — a function call adjusts the stack pointer and that's the allocation. But the stack's lifetime is the call frame; anything that needs to outlive the call must go on the heap.
Compilers get very smart about keeping things on the stack when they can prove the lifetime is bounded. Go does escape analysis: if a value doesn't escape its function, it stays on the stack. Java's scalar replacement and C++'s return value optimization do similar tricks.
Trade-offs by workload
- Latency-sensitive code (game engines, databases, embedded): manual or RAII. Predictable cleanup; no stop-the-world.
- Developer productivity and broad safety: GC. You don't think about memory most of the time.
- Systems where you want both: Rust. The borrow checker is the tax; the payoff is memory safety without a collector.
Failure-mode quick reference
| Model | Leaks possible? | Use-after-free possible? | Double-free possible? | Runtime cost |
|---|---|---|---|---|
| Manual (C) | Yes | Yes | Yes | None |
| RAII (C++) | Yes (cycles, long-lived containers) | Yes (raw pointers) | Rare | Minimal |
| Borrow checker (Rust) | Yes (Rc cycles) | No (safe Rust) | No (safe Rust) | None |
| GC (Java/Go/JS/Python) | Yes (reachable refs) | No | No | Pauses / throughput hit |
Leaks survive every model — if you keep a reference, no system will free the object for you. Everything else is catchable.