Code Story

Chapter #8 – Memory Management

stack vs heap, ownership, GC, reference counting, manual

8.0  Prologue

Every value a program creates occupies memory. How that memory is allocated, tracked, and freed is one of the most consequential design decisions in a language. The spectrum runs from fully manual control (C) through ownership systems (Rust) and reference counting (Swift, CPython) to tracing garbage collection (Java, C#, Go). Each approach trades off throughput, latency, safety, and programmer effort.

8.1  Stack vs Heap

The stack stores local variables and function call frames. Allocation is a single pointer decrement; deallocation happens automatically when the function returns. Stack storage is fast but limited (typically 1–8 MB per thread) and cannot outlive its frame. The heap provides dynamically allocated memory with programmer- or runtime-controlled lifetime. It is large (limited by virtual address space) but allocation and deallocation are expensive relative to stack operations, and fragmentation accumulates over time. // Rust: local variables on stack; Box<T> forces heap allocation let x: i32 = 42; // stack let b: Box<i32> = Box::new(42); // heap; freed when b is dropped // C++: new/delete for heap; local variables on stack int x = 42; int* p = new int(42); // heap; must call delete p Values that must outlive their creating scope, or whose size is unknown at compile time, must live on the heap. Containers (Vec, String, std::vector) allocate their elements on the heap while the container header itself may be on the stack.

8.2  Ownership and Borrowing

Rust’s ownership system enforces memory safety at compile time with no garbage collector. Three rules govern all values:
  1. Every value has exactly one owner.
  2. When the owner goes out of scope the value is dropped (memory freed).
  3. Values may be moved (ownership transfer) or borrowed (temporary reference).
let s1 = String::from("hello"); let s2 = s1; // s1 is moved; s1 is no longer valid let s3 = String::from("world"); let r = &s3; // shared borrow; s3 still owns the data println!("{r}"); // r valid here // s3 dropped at end of scope Borrowing rules: at any point in time, a value may have either any number of shared (immutable) references or exactly one exclusive (mutable) reference, but not both. These rules, checked at compile time, eliminate data races and use-after-free without runtime overhead. C++ move semantics (C++11) transfer resource ownership between objects but rely on programmer discipline; there is no compiler enforcement equivalent to Rust’s borrow checker.

8.3  Garbage Collection

A garbage collector automatically reclaims memory that is no longer reachable. Programmers do not call free or delete; the runtime periodically traces live objects and reclaims the rest. Tracing GC (Java, C#, Go) uses a mark-and-sweep or copying algorithm. Modern collectors are mostly concurrent (running alongside application threads) but still impose pause overhead, memory overhead (live set × 2–5× for efficient collection), and non-deterministic finalization. When GC shines: large object graphs with complex sharing patterns, rapid prototyping, applications where throughput matters more than latency. When GC hurts: real-time systems with hard latency bounds, embedded or resource-constrained devices, performance-sensitive inner loops.

8.4  Reference Counting

Reference counting tracks how many owners a heap allocation has. When the count reaches zero the memory is freed immediately. This gives deterministic destruction and avoids stop-the-world pauses, but cannot collect cycles. // Rust: Rc<T> (single-thread), Arc<T> (thread-safe) use std::rc::Rc; let a = Rc::new(String::from("shared")); let b = Rc::clone(&a); // reference count becomes 2 // both a and b freed when the last clone is dropped // Rust: Weak<T> breaks reference cycles use std::rc::Weak; let weak: Weak<_> = Rc::downgrade(&a); // C++: std::shared_ptr / std::weak_ptr auto sp = std::make_shared<int>(42); auto sp2 = sp; // ref count = 2 std::weak_ptr<int> wp = sp; // does not increment count CPython uses reference counting as its primary memory management strategy, supplemented by a cycle collector for objects that form cycles. Swift also uses automatic reference counting (ARC) compiled into the binary.

8.5  Manual Memory Management

C and pre-RAII C++ require explicit malloc/free or new/delete calls. The programmer is responsible for freeing every allocation exactly once. Failure modes are use-after-free, double-free, and memory leaks. RAII (Resource Acquisition Is Initialization) is the C++ idiom that ties resource lifetime to object lifetime. A destructor releases the resource when the object goes out of scope, making manual management manageable. Rust’s Drop trait is the direct equivalent; it is invoked automatically by the ownership system. // C: manual int* p = (int*)malloc(sizeof(int)); *p = 42; free(p); // must not forget; must not double-free // C++: RAII via unique_ptr eliminates manual delete auto p = std::make_unique<int>(42); // p freed automatically when it goes out of scope

8.6  Epilogue

Memory management strategy shapes a language’s safety guarantees, performance profile, and ergonomics. Rust’s ownership model achieves manual-level performance with GC-level safety. The next chapter covers how code is organized into modules and packages.

8.7  References

Rust Ownership - The Book
C++ Memory Management - cppreference
.NET Garbage Collection - Microsoft
CPython Garbage Collector - Dev Guide