Part IV · Memory Management — garbage collection and the alternatives

draft

Automatic memory management asks one question — *when is a piece of memory no longer reachable, and who reclaims it?* — and the answers split a language's entire performance, latency, and ergonomics profile. This chapter maps the field: the tracing-GC family, the reference-counting renaissance, mark-region collectors, and the approaches that avoid a runtime collector altogether. The decision for Koda lives in stack-RFC-034; this is the ground it stands on.


1. The tracing family (reachability from roots)

A tracing collector periodically finds what is reachable from a set of roots (stack slots, registers, globals) and reclaims the rest.

  • Mark-sweep — mark reachable, sweep the rest. Simple; fragments the heap.
  • Mark-compact / copying (semi-space) — moves live objects together, eliminating

    fragmentation and enabling bump-pointer allocation (fast). Cost: it moves objects, so it needs precise roots (know exactly which words are pointers) to update references.

  • Generational — the weak generational hypothesis: most objects die young. Collect a

    small young generation often (cheap) and promote survivors to an old generation collected rarely. A write barrier records old→young pointers. This is the workhorse of managed runtimes and the family Koda currently implements (young-gen copying + precise roots, #845#730#834).

  • Concurrent, low-pause — the modern frontier: collect while the program runs, keeping

    pauses sub-millisecond even on TB heaps.

    • Azul C4 — the pioneer of continuously-concurrent compaction.
    • ZGC (OpenJDK) — colored pointers + load barriers, region-based, concurrent

      compaction; generational ZGC landed in JDK 21 (2023).

    • Shenandoah (Red Hat) — concurrent evacuation via load-reference barriers.
    • Go GC — concurrent tricolor mark-sweep, non-moving, write barriers; trades

      compaction for simplicity and short pauses.

The cost of this family: barriers on loads and/or stores — real throughput overhead and deep implementation complexity.

Precise vs conservative roots. A conservative collector guesses which stack words are pointers (can't move objects safely; can leak). A precise collector knows exactly — a prerequisite for any moving/compacting scheme, and the substrate the reference-counting approaches below also want. (Koda's migration from conservative to precise roots is exactly this enabling investment.)


2. The reference-counting renaissance

Each object carries a count of references; when it hits zero, it is freed. For decades RC was dismissed as "slow and can't collect cycles." That verdict is out of date:

  • Deferred / coalesced / biased RC made the counting cheap (batch and skip redundant

    updates).

  • Perceus (Reinking, Xie, de Moura, Leijen — PLDI 2021): the compiler *nserts precise

    inc/dec at compile time, specializes drop, and — the key idea — does reuse analysis: when an object's refcount is provably 1, a "functional" update is compiled into an in-place mutation ("functional but in-place", FBIP). This makes RC competitive with or faster than tracing, and deterministic (no stop-the-world). Used in Koka and Lean 4 (a performance-critical theorem prover) and Roc*

  • LXR (Zuo, Blackburn et al. — PLDI 2022): RC + a backup Immix trace for cycles.

    Beats production tracing collectors (e.g. G1) on latency and throughput simultaneously — arguably the strongest GC result of recent years.

  • Nim ARC/ORC, Swift, Python — production imperative languages built on RC; ORC

    adds a cycle collector to ARC.

The catch: plain RC leaks cycles. Modern RC pairs with a backup cycle collector (LXR, ORC) or leans on ownership/uniqueness to make cycles rare. And Perceus's reuse magic is strongest in functional settings; in imperative/OOP languages the RC core still transfers cleanly (Swift/Nim prove it) but the automatic FBIP upside is partial and must be measured.


3. Mark-region and pluggable frameworks

  • Immix (Blackburn & McKinley — PLDI 2008): a mark-region collector — coarse regions

    with fine lines, great locality, and opportunistic evacuation to defragment. The basis of a large fraction of modern GC research (and of LXR's backup trace).

  • MMTk (Memory Management Toolkit, reimplemented in Rust): a framework that lets a runtime

    plug in different collectors behind one interface. Being integrated into OpenJDK, V8, Ruby, and Julia. The model to copy if a language wants to experiment with collectors without rewriting its runtime — but a heavy dependency to stand up.


4. Beyond GC — compile-time and ownership approaches

Some languages avoid a runtime collector by resolving lifetimes statically:

  • Ownership + borrow checking (Rust) — single-owner values freed deterministically at

    scope end; the borrow checker proves aliasing safety at compile time. Zero GC, zero pauses, predictable. Cost: real programmer burden (the borrow checker's learning curve).

  • Regions / arenas — allocate into a region, free the whole region at once. Manual in

    Zig/Odin; inferred in MLKit (Tofte-Talpin region inference) and Cyclone. Arena speed with no per-object bookkeeping.

  • Linear / affine types — types that enforce "use exactly once / at most once", enabling

    the compiler to insert deterministic frees. Austral, and the higher-RAII layer of Vale.

  • Generational references (Vale) — a genuinely different bet: cheap liveness checks via

    per-object generation numbers plus regions and single ownership, targeting memory safety without GC and without Rust's borrow checker. Promising but research-stage — not production-proven at scale.


5. How to choose

There is no universally "best" collector — the axes trade against each other:

Axis Tracing (concurrent) RC + reuse (Perceus/LXR) Ownership / regions
Pause / determinism low pause, not deterministic deterministic, no STW deterministic (compile-time)
Throughput vs Rust overhead (barriers, alloc) near-Rust if reuse fires =Rust (no runtime)
Cycles free needs backup collector n/a (no cycles by construction)
Programmer burden none none high (borrow checker) / medium (regions)
Maturity very high high (LeanKokaSwift) high (Rust) / experimental (Vale)

The greenfield-language angle. A brand-new language with full control of its compiler is the ideal place for compile-time-assisted memory management: the modern cohort of compiler-controlled languages did not choose tracing GC — Lean 4 and Koka chose Perceus RC + reuse, Rust chose ownership, Vale chose generational references. Tracing generational GC is the model of mature dynamic runtimes (Ruby, early Go), retrofitted where the compiler can't help. For a new language that also needs to beat a systems language on a hot path, this distinction is decisive.


Koder Stack anchor (D6 — decision lives elsewhere). Koda's own choice on this axis is not made in this compendium. It is recorded in stack-RFC-034 — Koda memory-management axis (engineering canon): tracing generational GC as the bridge to self-hosted-green, with Perceus-style precise RC + reuse as the long-term north-star, gated on a measured win against the self-hosted-first G2 performance gate (Koda ≤ Rust × 1.05 on a hot path). This chapter is the reference; that RFC is the decision — one fact, one home.