Garbage Collection

Overview

ƿit uses a tracing garbage collector for automatic memory management. Each actor owns its own heap, so collection is per-actor rather than global. Actors never share mutable heap objects, which keeps collection isolated to the actor that is currently running.

The collector is a build profile axis: the default copy profile uses a two-space copying collector, and the compact profile uses an in-place mark-compact collector for memory-constrained targets (see Profiles below). Both present identical language semantics; they differ only in memory footprint and pause shape.

Algorithm

The default (copy) collector uses a two-space copying strategy:

  1. Choose a fresh heap space for survivors.
  2. Copy all live root values into the new space.
  3. Scan copied objects and update their internal references.
  4. Release the old heap space once all live values have been copied.

This makes allocation cheap: ordinary heap allocation is a pointer bump until the actor reaches its current heap limit.

Forwarding

When an object is copied, the collector leaves behind a forwarding marker in the old space. If another reference reaches the old address later in the same collection, the collector follows the marker to the already-copied object instead of copying it again.

Conceptually:

old heap object -> forwarding marker -> copied object in new heap

Forwarding is internal to collection. User code never observes forwarding objects directly.

Scan Phase

After roots are copied, the collector scans the new heap linearly. For each copied object, it examines the values stored inside it:

  • References into the old actor heap are copied or forwarded.
  • References to runtime constants outside the actor heap are left alone.
  • Immediate values such as small numbers, logical values, null, and immediate text do not need tracing.

The scan ends when every copied object has been visited.

Roots

The collector starts from the actor’s live roots:

  • Runtime context values
  • Current exception/disruption state
  • Active VM frames and register slots
  • Values held by native calls through explicit runtime roots
  • Constant pools and compiler/runtime temporaries that are active during the current turn

Native code that keeps a heap value across an allocating operation must root that value. Otherwise the collector may move or reclaim it.

Per-Actor Heaps

Each actor’s heap is collected independently:

  • No global stop-the-world collection is required.
  • One actor’s collection does not scan another actor’s heap.
  • Message passing copies values on send, so each actor collects only its own heap state.
  • Heap sizes adapt independently based on each actor’s live data and allocation pressure.

Heap Sizing

After a collection, the runtime compares the live set with the actor’s current heap capacity and adjusts the next target. Actors that repeatedly recover too little space are given more room; actors with smaller live sets can shrink back toward a lower capacity.

This policy is intentionally adaptive rather than fixed. The goal is to keep allocation fast while avoiding permanently oversized actor heaps.

Collector Profiles

The collector implementation is selected per build (the recipe’s stamp.gc, folded into the runtime profile string so build artifacts never cross collector profiles):

  • copy (default) — the Cheney two-space collector described above, plus an optional generational nursery with a card-table write barrier. Fast collects, but each collection transiently holds a second heap block, so committed memory can approach twice the heap size.
  • compact — an in-place threaded mark-compact collector (Jonkers pointer threading). Live objects are marked through a side bitmap, every reference is threaded through its target’s header, and two linear passes update references and slide the live objects down in place. It works within a single heap block, so committed memory stays close to the live set plus allocation slack, at the cost of slower collections. The compact profile collects the whole heap each time: a minor-collection request performs a full collect, and the write barrier compiles away.

Under both profiles the observable behavior is the same: precise per-actor collection from the same root set, and class finalizers for unreachable records. There is no intern table for the collector to sweep — text and shape identity come from Mach images, which live outside the heap entirely.

Relationship to Stone Values

Stone is an immutability property, not a heap membership property. Stoned heap objects that belong to an actor are copied by that actor’s collector like other heap objects. Runtime constants that live outside actor heaps are left unchanged when the scanner encounters them.

See Stone Values for the immutability model and Mach for how VM slots keep heap values reachable.