Every variable your program creates lives somewhere in memory. Where it lives — and who is responsible for freeing it — is one of the most consequential decisions in language and runtime design. Get it wrong and you get segfaults, memory leaks, use-after-free vulnerabilities, or unpredictable GC pauses. Get it right and you get programs that are both safe and fast.
What changed in 2026
- Rust adoption in systems and web contexts grew significantly. Rust is now used in the Linux kernel, Windows kernel, Android, and major cloud providers' control planes. Its ownership model is no longer exotic.
- Safety-focused C++ standards (C++26) borrowed ideas from Rust. Lifetime annotations and improved smart-pointer guidelines are part of the C++26 safety profile.
- WebAssembly linear memory management matured. WASM modules manage a flat byte array; languages targeting WASM (Rust, C, AssemblyScript) handle allocators differently than on native platforms.
- Memory-safe languages became a US government recommendation. The CISA/NSA guidance recommending memory-safe languages drove more enterprise adoption of Rust, Swift, and Go over C/C++.
The memory regions
A running process has several memory regions:
| Region |
Contents |
Who manages |
| Text segment |
Compiled instructions |
OS / loader |
| Data / BSS |
Static and global variables |
Compiler |
| Stack |
Function frames, local variables |
CPU / compiler |
| Heap |
Dynamic allocations |
Programmer / GC / allocator |
| Memory-mapped |
Files, shared memory, mmap |
OS |
Stack allocation
The stack is a LIFO structure. Each function call pushes a frame; return pops it. Allocation and deallocation cost a single pointer adjustment — effectively free.
void foo() {
int x = 42; // stack-allocated; gone when foo() returns
char buf[1024]; // 1 KiB on the stack; fine for small buffers
}
Limits: stack size is typically 1–8 MiB per thread. Deep recursion (or large stack arrays) causes a stack overflow. Objects cannot outlive their enclosing function frame.
Heap allocation
The heap is managed dynamic memory. malloc/free (C), new/delete (C++), and language runtimes all ultimately call into an allocator like jemalloc, tcmalloc, or mimalloc.
char *buf = malloc(1024 * 1024); // 1 MiB on the heap
if (!buf) { /* handle OOM */ }
// ... use buf ...
free(buf); // must free exactly once
Common heap bugs:
- Leak — allocate, never free.
- Use-after-free — access memory after
free(); undefined behavior.
- Double-free — call
free() twice on the same pointer; crashes allocator.
- Buffer overflow — write past the allocated region; corrupts adjacent memory.
Rust ownership model
Rust eliminates these classes of bugs at compile time through ownership rules:
- Every value has exactly one owner.
- When the owner goes out of scope, the value is dropped (freed).
- You can borrow a reference (
&T for read, &mut T for write), but borrows must not outlive the owner and there can be at most one mutable borrow at a time.
fn main() {
let s = String::from("hello"); // s owns the heap allocation
let r = &s; // immutable borrow; s still owns
println!("{}", r);
// s is dropped here; heap memory freed automatically
}
// This fails at compile time — borrow checker catches it:
// let r;
// {
// let s = String::from("hello");
// r = &s; // ERROR: s does not live long enough
// }
// println!("{}", r); // use-after-free prevented
Virtual memory
Modern OSes use virtual memory: each process gets its own address space (0 to ~128 TiB on x86-64). The kernel maps virtual pages to physical RAM pages on demand.
Virtual address space (per process)
0x0000_0000_0000 – stack, heap, code
...
physical RAM pages only mapped when accessed (demand paging)
Implications for allocators: you can mmap a large region (e.g., 1 GiB) without immediately using physical RAM. malloc implementations do this to reserve space and commit pages lazily.
Smart pointers in C++ and Rust
Both C++ and Rust offer smart pointers that automate heap management without a GC:
| Smart pointer |
Language |
Semantic |
unique_ptr<T> |
C++ |
Exclusive ownership; free on drop |
shared_ptr<T> |
C++ |
Reference-counted shared ownership |
Box<T> |
Rust |
Exclusive heap ownership |
Arc<T> |
Rust |
Atomic ref-counted shared ownership |
Rc<T> |
Rust |
Single-thread ref-counted ownership |
How to pick your memory management strategy
- Use a GC language (Go, Java, Python, JS) when developer velocity and safety matter more than pause-free latency.
- Use Rust when you need both safety and predictable low latency — systems, embedded, WebAssembly.
- Use C++ with modern smart pointers when you are in an existing C++ codebase and cannot switch.
- Profile before optimizing — heap allocation is rarely the bottleneck; cache misses usually are.
- Use a custom allocator (jemalloc, mimalloc) if profiling shows
malloc contention in a multithreaded hot path.
Common mistakes
Stack-allocating large arrays. char buf[10 * 1024 * 1024] will overflow the stack on most platforms. Heap-allocate anything over ~64 KiB.
Ignoring allocator fragmentation. Long-running services that allocate and free many small objects can end up with a fragmented heap where RSS grows unboundedly. Tools like jemalloc with jemalloc.stats.print() help diagnose this.
Mixing allocators across library boundaries. Allocating in one DLL / shared library and freeing in another crashes when the libraries use different heaps (especially on Windows).
Assuming free returns memory to the OS. Most allocators hold freed memory in their own free list and return it lazily. RSS can stay high even after free().
Not using AddressSanitizer during development. ASan finds use-after-free, buffer overflow, and double-free bugs at runtime with ~2× overhead — run it in CI.
What to skip
- Manual memory management in new Go, Python, or Java code — you do not need it; let the runtime manage the heap.
unsafe Rust in business logic — reserve it for FFI boundaries and proven hot paths.
- Writing your own allocator without profiling data showing the system allocator is the bottleneck — it almost never is.
FAQ
What causes a segfault?
A segfault occurs when a process accesses a virtual address that has no valid physical mapping — typically a null pointer dereference, a stack overflow, or a use-after-free that landed outside mapped pages.
What is the difference between RSS and VSZ?
RSS (Resident Set Size) is the actual physical RAM pages in use. VSZ (Virtual Size) is the total virtual address space mapped, including pages not yet backed by physical RAM. RSS is the number that matters for capacity planning.
How does a garbage collector know what is live?
It starts from roots — stack variables, globals, and CPU registers — and traces every pointer reachable from those roots. Anything not reachable is considered dead and its memory reclaimed.
Can you have both a GC and manual allocation in the same program?
Yes. Java's Unsafe, Go's syscall.Mmap, and Python's ctypes.create_string_buffer all allow manual off-heap allocation alongside the GC-managed heap. Use carefully.
Where to go next