module 01 · memory models · Part IV · the payoffs

Chapter 23 — The unification: one table, every language, every payoff

The 30-second version. Twenty-two chapters ago, memory was a mystery; now it is a design space with named axes. Every language is a point in that space, chosen under constraints you can reconstruct. This chapter compresses the whole module into one comparison table, traces the connections forward into every queued module, and turns the knowledge into a working toolkit: the profilers, the flags, the practice path, and the questions that separate people who have this module from people who don't.

The four questions, one last time

Every language's memory model is its answers to four questions — you can now interview any language in four sentences:

  1. What is a value? (unboxed bytes / boxed header+payload / tagged word — Chapter 06)
  2. Who owns it? (the programmer / the scope / the last owner / the collector / nobody, it's static — Chapters 08-18)
  3. Who frees it, and when? (explicitly / at scope exit / on drop / tracing / refcount / never — the whole Part II story)
  4. What is promised about safety? (nothing / compile-time aliasing guarantees / runtime bounds checks / managed + unsafe edges — Chapters 08, 10, 21)

The grand table

Read a row, then a column — the table is both a reference and a map of design trade-offs spanning sixty years:

LanguageValue repr.HeaderOwnership / reclamationPer-int cost*Safety floorWhere freed bytes goKiller chapter detail
Cunboxednonemanual free4 Bnone (UB)allocator free-listsmetadata at p−8 (ch08, 19)
C++unboxed + RAIIvptr only (virtuals)scope/dtor; smart ptrs4 Bnone, patterns helpdtor → free-listsmake_shared one block (ch09)
Rustunboxed, movesnoneownership; drops at scope4 Bcompile-time (safe code)drop glue → allocatorniche: Option<&T>=8B (ch10)
Gomostly valuenoneGC (tricolor, concurrent)4 B (int), iface boxesGC + bounds checkssize-class spansescape analysis (ch11)
Javaboxed12 B (compressed)GC, generational16 B (Integer)GC + checks + JITeden→survivors→oldcompressed-oops 32GB cliff (ch12)
C#values or boxed8 B (boxed)GC, gen + LOH4 B struct / 24 B boxedGC + checksgen0 bump → gen2/LOHLOH 85,000B, never compacts (ch13)
Pythonboxed PyObject16 B (refcnt+type)refcount + cycle GC28 Bnever crash, always boxedpymalloc pools/arenas510-ish is artifacts (ch14)
JS (V8)Smi / heap obj8 B map wordGC scavenger+mark-compact8 B (Smi word)GC + checkssemispaces → old spacehidden-class transitions (ch15)
Swift/ObjCvalue + ARC refsisa (8 B, packed)compiler-inserted refcounts4 B Int / 16 B String SSOARC + bounds checksrefcount 0 → freeweak zeroing via side tables (ch16)
COBOLfixed record bytesnonestatic, never freedPIC bytescompile-time layoutnothing is ever freedCOMP-3 nibbles (ch17)
Fortran (classic)static + COMMONnonestatic4 Bcompile-timenothing is ever freedcolumn-major (ch18)
Lisptagged constag bitsGC (invented here)2-word consGCsemispace copy (1959)half-machine GC (ch18)
Haskellboxed thunksheadergenerational GC~24 B/thunkGC, puritycopying + compactionspace leaks (ch18)
Erlangboxed per-processheaderper-process copying GCboxedshare-nothing isolationprocess death = free>64B binaries refcounted (ch18)
Zigunboxednonemanual via passed allocator4 Bexplicit + arenasyour allocator, greppabledefer arena.deinit() (ch18)

\* 64-bit, order-of-magnitude, platform-per-chapter caveats apply — every chapter states its exact numbers.

Three columns deserve a second read. Per-int cost: the 4→28-byte spread between C and Python is the entire "interpreted vs compiled" performance conversation in one number — it is representation (Chapter 06), not magic. Where freed bytes go: "never" (COBOL/Fortran) through "free-lists" (C world) to "copied and compacted" (GC world) — that axis is the fragmentation story of Chapter 05. Killer detail: each language's most distinctive memory fact, the one that identifies a practitioner who has actually been inside.

The derivation web: how the models descend from the machine

The module's spine, redrawn as one lineage:

silicon (ch01) → hierarchy (ch02) → virtual memory (ch03)
        │                                    │
   stack for nested lifetimes (ch04)    heap for everything else (ch05)
        │                                    │
        └────────── object layout (ch06) ────┘
                         │
                assembly ground truth (ch07)
                         │
   ┌──────────┬─────────┴──────┬──────────────┬─────────────┐
   manual      RAII       ownership      GC-family     refcount-family
   (C ch08)   (C++ ch09) (Rust ch10)  (Go/Java/C#/JS) (Python/Swift)
   └──── static family: COBOL/Fortran — no heap at all (ch17, ch18)
                         │
       allocators under everything (ch19) → hardware memory model (ch20)
                         │
        failure = security surface (ch21) + platform economics (ch22)

Every arrow is a forced move, not a fashion: GC exists because tracing beats refcount cycles (Chapter 14); compaction exists only where pointers are enumerable (Chapters 05, 12); ownership exists because compile-time is the only time you get safety for free (Chapter 10); mobile killing exists because swap can't (Chapter 22). If you can redraw this figure from memory, the field is yours.

The toolbox: see every model with its own eyes

The labs across the module, condensed into one cheat-sheet:

QuestionToolChapter lab
Where do my bytes live? (C)/proc/self/maps, strace -e brk,mmap03, 05, 08
Is it a leak, fragmentation, or retention?RSS-vs-heap-profiler method; malloc_stats; MALLOC_ARENA_MAX05, 19
Catch UB before prod-fsanitize=address,undefined in CI08, 21
Did the compiler escape-allocate it? (Go)go build -gcflags='-m'11
Why is GC running? (JVM)-Xlog:gc*, JOL, jmap -histo12
Object sizes? (CLR)dotnet-counters, GC.GetGeneration13
Who retains this? (Python)tracemalloc, gc.get_referrers14
Heap graph + retainers? (JS)DevTools heap snapshot, Retainers panel15
Retain cycles? (Swift)Xcode Memory Graph debugger16
Cache behavior, false sharingperf stat, perf c2c, alignas(64)02, 20
Concurrency memory bugsThreadSanitizer; litmus-test harnesses20
Exploit-readiness of a binarychecksec, gdb, ROP gadget scanners21
Mobile footprint truthdumpsys meminfo (PSS) / MetricKit jetsam events22
The frame in the fleshgdb disas, info frame, x/8gx $rsp04, 07

The meta-lesson: every memory question has an instrument. Teams that debug by staring at code are choosing blindness in a field where the truth is one command away.

How to practice (the path that makes it permanent)

Reading fades; building stays. Ranked by leverage:

  1. Write a bump allocator (15 lines — Chapter 19). Then a pool allocator. Then a size-class free-list. You will never again find allocators mysterious, and Chapters 11-15's runtime internals become your design decisions.
  2. Write a toy tracing GC in C or Rust: 2 semispaces, 512 objects, one root array, Cheney's scan (Chapter 12's diagram is the whole algorithm). You have now built what JVM/V8/ART run daily — and Chapter 15's forwarding-pointer map word will click forever.
  3. Refcount with a cycle detector: Python's algorithm, 100 lines (Chapter 14's gc_refs subtraction). Watch it find the cycle your manual refcounts leaked.
  4. Break each thing once: overflow a canary, double-free into tcache, false- share a counter, race a data-race litmus, leak a closure over a DOM node — each in a sandbox, each observed with its instrument (Chapter 21's defensive labs). Security understanding at this level is just memory-model knowledge pointed the other way.
  5. Read one real crash per week with gdb + the frame grammar (Chapters 04, 07). In a quarter you will be the person on the team who reads core dumps.

The forward connections: why this was module 01

Every queued module is this module wearing another costume:

That is the entire program thesis in one paragraph: memory is the root layer of computer science, and every field is a branch that forgot it has roots.

What insiders know (the module-sized version)

Quiz — the final eight (cross-module synthesis)

  1. Order these by memory-safety strength for engine-level code, and justify: C, Rust (safe), Java, C++ with RAII + sanitizers in CI.
  2. A polyglot service leaks in production: Rust core, Python orchestration, JS frontend graphs, a C FFI codec. Describe your triage sequence across the modules' tools.
  3. Why does the same numeric workload cost ~7x more memory in Python than C? Answer with representation, not adjectives.
  4. fork() is fast, zygote app-launch is fast, JVM -Xmx reserves huge virtual space, and mmap'd files cost ~nothing resident — name the single Chapter 03 mechanism doing all four.
  5. An exploit needs: a heap-adjacent metadata fact (ch19), an address-layout fact (ch03), a frame-layout fact (ch04), and a representation fact (ch06). Name one concrete fact of each and the attack phase it serves.
  6. Why do Go, Java, C#, V8, and ART all reach the same allocator design (bump + size classes + per-thread caches) despite five different GCs? Derive it from Chapters 05+19+12.
  7. One sentence each: why does (a) Python's gc module exist, (b) Swift's weak references zero themselves while Rust's cannot, (c) COBOL forbid recursion (pre-2002), (d) Android kill apps where desktops swap?
  8. You get one hour to teach a junior "why is our p99 slow under load." Build the lesson plan from this module's chapters.
Answers
  1. From weakest: C (nothing enforced) < C++/RAII+ASan (patterns + tooling, still UB-capable where patterns lapse) < Java (memory-safe at the language level, but engine/JNI/FFI edges corrupt — ch21's mirage) ≈ Rust safe (compile-time guarantee, no GC pauses, but unsafe and FFI edges remain). The honest ranking is about where the guarantees are enforced: nowhere < conventions + CI < a runtime < the type system — and every tier still leaks (retention) even when it cannot corrupt.
  2. Measure first (RSS/PSS trend — ch03/22), then per-runtime: Rust — ASan/heaptrack on the core (ch08/19); Python — tracemalloc + gc.get_referrers for cycles and closures (ch14); JS — DevTools heap snapshot, Retainers panel for detached DOM (ch15); C FFI — valgrind + reference-leak discipline (ch08); then the cross-cut suspects: per-thread caches not released (ch19), allocator arena retention (ch05/19), and false-positive "leaks" that are fragmentation plateaus (ch05). The sequence is: which tax (ch05), which runtime, which instrument.
  3. Representation: every int is a boxed PyObject — 16-byte header (refcount + type pointer) + 28 bytes total for a value C stores in 4 (ch14 vs ch08); plus pointer indirection per access (ch06), plus pymalloc's 16-byte size-class rounding (ch14), plus per-object cache-line footprint (ch02). It is header + indirection + rounding + cache, each a chapter, together ~7x.
  4. Demand paging / mapped pages with copy-on-write: fork copies page tables not pages (COW sharing); zygote's boot image is a persistent COW parent (ch22); -Xmx reserves virtual, commits lazily — RSS ≠ VSZ (ch03/12); file-backed mappings are resident only while hot, evictable clean pages (ch03).
  5. Ch19: freed chunk's size/fd fields sit adjacent to user data (tcache fd at chunk+16). Ch03: ASLR randomizes the PIE/mmap bases — defeat via an info leak. Ch04: the saved return address is at [rbp+8], past locals and the canary. Ch06: length is forgotten at array decay — the overflow precondition. Phases: the overflow (06) corrupts metadata (19), survives mitigation layout (03/04), and redirects control flow. This is Module 02's opening lecture.
  6. Bump allocation is O(1) and packs locality (ch05/02); the generational bet (ch12) makes most objects die young, so the nursery is bump-allocated and swept wholesale; per-thread caches remove the shared lock (ch05/19); size classes make free O(1) and bound fragmentation without compaction (ch19). Five GCs, one conclusion: the allocator under a GC is still Chapter 05's machine, and the physics picks the same winner each time.
  7. (a) Refcounting cannot free cycles — the gc module's mark-from-suspects collector does (ch14). (b) Swift's weak slots live in a global side table, zeroed during dealloc before the memory dies — outside the object, so an outside writer can nil them; Rust's weak count lives inside the Rc block the dying object occupies — nobody can zero what is about to be freed (ch16/10). (c) With no per-invocation stack, recursion would share WORKING-STORAGE across re-entries — corrupted state; LOCAL-STORAGE (a real stack) is the 2002 opt-in (ch17). (d) No swap on flash + battery + shared-pressure economics: eviction by killing priority bands is the page-replacement policy of last resort (ch22).
  8. The plan: (i) which tower level — perf stat, cache-misses (ch02); (ii) is it allocation rate driving GC/collector cadence (ch11/12/22); (iii) false sharing on shared counters (ch20/02); (iv) lock contention masquerading as memory (per-thread caches — ch19); (v) frame it as the three taxes (ch05) and confirm with the per-runtime instrument (the toolbox table). One hour, five instruments, every answer verifiable — that is the module's whole promise.