Garbage collection

The collector is a non-moving generational tracing collector with an incremental old-gen mark phase. Short-lived allocations live in a young-generation nursery; values that survive a minor collection are promoted to old-gen, which is marked in paced slices between mutator allocations. A write barrier tracks old-to-young pointers so minor collections stay proportional to young reachability. Any mino function that allocates may advance the collector, which is why borrowed values can become invalid after the next call.

Objects survive collection if they are reachable from a root: registered environments, host refs, the module cache, vars, namespace env tables, compiled bytecode constants, or the C stack (via conservative scanning). The symbol and keyword intern tables are weak: they hold entries by value but do not pin them across major cycles. An interned symbol survives only as long as some other root still references it; the next intern of the same string after a sweep that pruned the previous entry returns a fresh value. See the Value retention section of the Embedding Guide for how to keep host-held values reachable across collection.

Design

Four choices shape the collector:

Phases

The collector runs in one of four phases, reported as the :phase key of (gc-stats) and the phase field of mino_gc_stats_t:

Both barriers -- the remembered-set barrier that tracks old-to-young edges, and the insertion barrier that captures new slot values during major mark -- are always armed. Their cost is one dirty-bit check per store in the common case.

Transitions between phases:

                    nursery full
                   or explicit MINOR
             +------------------------+
             |                        v
          [idle] <-----------+    [minor]
             |               |        |
             |               +--------+ sweep done
             |
             | threshold reached
             | or explicit MAJOR/FULL
             v
       [major-mark] <--+
             |         |
             | slice   | more work
             +---------+
             |
             | mark drained
             v
       [major-sweep]
             |
             | sweep done
             v
          [idle]

A minor cycle can nest safely inside major-mark: when the nursery fills during an in-flight major, minor runs to completion and returns to major-mark without disturbing the outer mark stack. MINO_GC_FULL from the idle state runs a minor, then a complete major cycle back-to-back.

Host-driven collection

The host can trigger collection at quiescent points -- between REPL turns, after bulk import, or before long-idle periods -- through mino_gc_collect:

mino_gc_collect(S, MINO_GC_MINOR);  /* nursery sweep only */
mino_gc_collect(S, MINO_GC_MAJOR);  /* drain or run a major cycle */
mino_gc_collect(S, MINO_GC_FULL);   /* minor + full STW major */

Tuning

Five knobs tune the collector. Defaults work for most embedders; adjust only with a measurement in hand.

mino_gc_set_param(S, MINO_GC_NURSERY_BYTES,       2 * 1024 * 1024);
mino_gc_set_param(S, MINO_GC_MAJOR_GROWTH_TENTHS, 15);  /* 1.5x */
mino_gc_set_param(S, MINO_GC_PROMOTION_AGE,        1);
mino_gc_set_param(S, MINO_GC_INCREMENTAL_BUDGET,   4096);
mino_gc_set_param(S, MINO_GC_STEP_ALLOC_BYTES,     16 * 1024);

Each setter returns 0 on success and -1 on a bad parameter or out-of-range value. Accepted ranges:

ParameterDefaultMinMaxEffect
NURSERY_BYTES4 MiB64 KiB256 MiBLarger = fewer minor cycles, more work per cycle, higher peak pause. Default rose from 1 MiB to 4 MiB in v0.250.0 after realistic_bench measurement; allocation-heavy workloads gained 1.14–1.42x with no measurable pause regression.
MAJOR_GROWTH_TENTHS15 (1.5x)11 (1.1x)40 (4.0x)Old-gen growth above baseline before the next major fires.
PROMOTION_AGE118Number of minor survivals before a young object promotes to old.
INCREMENTAL_BUDGET40966465536Headers popped from the mark stack per major slice. Higher = longer slice, fewer slices.
STEP_ALLOC_BYTES16 KiB102416 MiBBytes allocated between automatic major slices. Lower = more frequent slicing.

Stats

Query collector counters via a plain out-struct. No allocation is performed.

mino_gc_stats_t st;
mino_gc_stats(S, &st);
printf("live=%zu minor=%zu major=%zu max_pause_ns=%zu\n",
       st.bytes_live, st.collections_minor,
       st.collections_major, st.max_gc_ns);

The same data is available from mino via (gc-stats), which returns a map of keyword keys. Full field list:

Environment variables

Four environment variables configure the collector at state init without touching source:

Next steps