Overview
The counterpart of Java's memory anatomy
reads very differently: there are no runtime-managed regions to diagram, because
where a value lives is written in its type. Locals live on the stack; Box<T> is a
heap allocation; Rc/Arc are heap allocations with reference counts; and when the
last owner drops, the free happens right there, compiled into the code. Where
Go's escape analysis decides stack-vs-heap invisibly and the
JVM decides everything at runtime, Rust surfaces the decision as API — which is why
this note is mostly a tour of types, not of runtime machinery.
Key points
- Stack by default, heap by request: values live inline (locals, struct fields —
no per-object headers, no pervasive pointer-chasing);
Box<T>moves one value to the heap (recursion, huge values, trait objects);Vec/Stringown heap buffers with (ptr, len, capacity) on the stack. - Layout is optimized, not stable:
repr(Rust)may reorder fields to minimize padding; niche optimization packs enums into invalid bit patterns —Option<Box<T>>,Option<&T>,Option<NonZeroU32>are all pointer-sized, the null-pointer optimization generalized.repr(C)opts into stable layout for FFI. - The sharing ladder:
&T(borrowed view, free) →Box<T>(one owner) →Rc<T>(shared ownership, single-thread, non-atomic counts) →Arc<T>(atomic counts, crosses threads) → addWeak<T>to break cycles (caches, parent pointers). Climb only as far as the design forces; each rung costs. Cow<'a, T>defers own-vs-borrow to runtime — borrow when unchanged, allocate only on write; the workhorse of zero-copy parsing and normalize-if-needed APIs.- Interior mutability is the sanctioned escape hatch: shared-reference mutation
through types that re-impose the rules differently —
Cell<T>(copy in/out, zero cost),RefCell<T>(borrow rules checked at runtime, panics on violation),OnceCell/LazyLock(write-once init, thestaticinitialization answer),Mutex/RwLock/atomics (the thread-safe rungs — see fearless concurrency).Rc<RefCell<T>>is the single-threaded "shared mutable object" idiom — and its panics are the price. - Safety ≠ no leaks:
mem::forget,Box::leakandRccycles all leak safely — leaking is not memory-unsafety. Long-lived services still watch growth; the heap-profiling tools (dhat, heaptrack) andWeakdiscipline are the answers. - Drop order is specified: locals in reverse declaration order, fields in
declaration order, and
drop(value)forces early release — relevant for lock guards and RAII handles; alet _guard = …vslet _ = …distinction that bites exactly once per career. - Allocators are swappable: the global allocator (system malloc by default) can
be replaced with one attribute — jemalloc/mimalloc for multithreaded churn; no
compaction exists, so long-running fragmentation behaviour is the allocator's
problem, not a GC's.
#[global_allocator]is also how dhat instruments.
Details
The decision table
| Need | Reach for | Cost |
|---|---|---|
| Pass a value without giving it up | &T / &mut T |
Free — this is the default |
| Heap-place one value (recursion, dyn Trait) | Box<T> |
One allocation |
| Several owners, one thread | Rc<T> (+ RefCell if mutated) |
Non-atomic count; runtime borrow checks if RefCell |
| Several owners, across threads | Arc<T> (+ Mutex/RwLock if mutated) |
Atomic count; lock contention |
| Break an ownership cycle | Weak<T> |
Upgrade check on access |
| Maybe-modify borrowed data | Cow<'_, T> |
Allocation only on write |
| Write-once global / lazy init | OnceCell / LazyLock |
One-time synchronization |
Mutate through &self, one thread |
Cell (Copy) / RefCell |
Zero / runtime checks & panic risk |
Contrast with the managed runtimes
The JVM buys allocation speed with TLAB bump-pointers and pays in collector
complexity; Go buys simplicity with escape analysis and a low-pause collector and pays
~CPU% forever. Rust's trade: allocation and free are explicit-but-inferred (no
collector, no barriers, no pauses — the predictability embedded and latency-critical
systems buy), and the price is this note's decision table living in your head instead
of in a runtime. Fragmentation replaces GC tuning as the long-service memory concern;
Arc count traffic replaces card tables as the sharing overhead.
Examples
// Niche optimization — no overhead for the Option:
assert_eq!(size_of::<Option<Box<u64>>>(), size_of::<Box<u64>>());
// The single-threaded shared-mutable idiom, runtime-checked:
let registry: Rc<RefCell<HashMap<Id, Node>>> = Rc::new(RefCell::new(HashMap::new()));
registry.borrow_mut().insert(id, node); // panics if a borrow is live
// Guard scope — the classic drop-order bite:
let _guard = mutex.lock().unwrap(); // held to end of scope
let _ = mutex.lock().unwrap(); // dropped IMMEDIATELY — lock released
Related
- Ownership, memory & concurrency — the model — parent map of the group.
- Ownership & borrowing — the rules that make static cleanup sound.
- Fearless concurrency —
Arc,Mutexand atomics: this ladder's thread-safe rungs. - Profiling & debugging — dhat and heaptrack, when growth needs explaining.
- JVM memory anatomy — the runtime-managed contrast, region by region.
- The Rust Programming Language — ch. 15 (smart pointers) is this note's canonical source.
Citations
[1] The Book, ch. 15 — Smart Pointers [2] The Rustonomicon — Data Layout [3] std::cell documentation — the interior-mutability catalog