Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

monoruby has its own garbage collector: a non-moving, single-threaded, stop-the-world, generational mark-and-sweep collector, modeled on CRuby’s RGenGC. This page is an overview; the full design document is doc/gc.md, and the implementation lives in monoruby/src/alloc.rs.

Heap layout

  • All heap objects (RValue) are exactly 64 bytes. Memory comes from a single 2 GB virtual arena reserved up front, carved into 256 KB pages of 4032 cells each.
  • Mark bits and old bits are stored outside the object cells, as per-page bitmaps. A pointer’s page is found with a single address mask, so bitmap lookup is O(1).
  • Objects never move, so raw *const RValue pointers stay valid across collections.
  • Allocation pops from a free list when possible, otherwise bump-allocates in the current page. The JIT inlines this free-list fast path directly into compiled code.

Generational collection

Minor collections trace only young objects; old objects are assumed live and their mark bits are seeded from the old bitmap.

  • Objects of promotable types (Object, String, Array, Hash, Bignum, Float, Struct) age by one on each minor GC they survive; at age 3 they are promoted to the old generation.
  • Write barrier: when a reference is stored into an old object, a single header-bit test decides whether the object must enter the remembered set, whose old→young edges are traced during minor GCs. The JIT emits the barrier inline; bulk operations (Array#concat, …) use a bulk variant.
  • A major (full) collection runs when the old-object count crosses an adaptive threshold or after 64 consecutive minors; it clears all generation state and retraces everything. GC.start always forces a major.

The write-barrier / remembered-set interaction (including why a minor GC without the barrier would sweep live objects) is illustrated in doc/gc_write_barrier.svg.

GC triggers and safepoints

Collections are requested by setting a single per-thread alloc_flag, and performed only at safepoints:

  • Allocation pressure — every ~8 filled pages trips the flag.
  • malloc pressure — a custom #[global_allocator] tracks off-heap allocation (String/Array backing stores etc.) and requests a GC when it outgrows an adaptive threshold, so heavy malloc traffic can’t outrun the heap-cell trigger.
  • ExplicitGC.start.

Safepoint polls (compare alloc_flag; conditionally call gc) are emitted at callee entry and loop back-edges in both the VM tier and JIT code. The same poll also drives green-thread preemption and pending-signal delivery (see Threads and Fibers). Rooting is precise: roots are explicitly enumerated — the executor’s frame chain, temporary-value stack, the green-thread scheduler’s thread registry, pending exceptions, and global state — never conservatively scanned off the machine stack; JIT code spills live registers before a safepoint call.

Controlling the GC

ControlEffect
GC.startForce a full (major) collection
GC.enable / GC.disableToggle collection at runtime
--no-gc CLI flagDisable GC for the process
GC.count / GC.statCollection counters / CRuby-compatible stats

Debugging Cargo features: gc-log (stats at exit), gc-debug (assertions), gc-stress (collect on every allocation — used by bin/test in CI), gc-verify (independent re-mark verification after each minor GC).

Further reading

  • doc/gc.md — full design document (heap layout, bitmaps, aging/promotion, remembered-set self-cleaning, heap-escaped frame reclamation)
  • doc/safepoint.md — the safepoint / poll-flag mechanism shared by GC, preemption, and signals