Open-Source Wikis

/

Ruby

/

Systems

/

Garbage collector

ruby/ruby

Garbage collector

Ruby's garbage collector is a generational, incremental, optionally-compacting mark-and-sweep collector that lives behind a pluggable interface. The default in-tree implementation is gc/default/gc.c. An experimental MMTk-based binding lives in gc/mmtk/. The high-level dispatcher (gc.c at the repo root) routes calls to whichever implementation was selected at build time.

Purpose

  • Allocate and free Ruby objects.
  • Mark live objects from the roots (VM stack, globals, registered roots, finalizers).
  • Sweep dead objects and reclaim their slots.
  • Optionally compact: relocate objects to defragment the heap.
  • Cooperate with write barriers so the generational and incremental collectors can avoid scanning the entire heap on every cycle.
  • Expose GC.* Ruby methods (gc.rb on top of C primitives in gc.c).

Files

File Purpose
gc.c High-level dispatcher. Calls into the selected GC. ~161 KB.
gc.rb Ruby-level GC.* API. Compiled into the binary.
gc/gc.h Public GC interface header.
gc/gc_impl.h Implementation contract: a struct of function pointers each GC must populate.
gc/default/ The in-tree GC. ~16,000 lines of C.
gc/default/gc.c The actual algorithm.
gc/mmtk/ MMTk binding (Rust + C glue). Optional.
internal/gc.h Internal helpers used by the rest of CRuby.
weakmap.c ObjectSpace::WeakMap and ObjectSpace::WeakKeyMap.
ext/objspace/ The user-facing introspection extension (ObjectSpace.dump_all, etc.).

Pluggable interface

gc/gc_impl.h declares the contract every GC implementation must implement:

typedef struct rb_gc_impl_s {
    void *(*objspace_alloc)(void);
    void  (*objspace_init)(void *objspace);
    void  (*objspace_free)(void *objspace);
    VALUE (*newobj_of)(void *objspace, VALUE klass, VALUE flags, ...);
    void  (*mark)(void *objspace, VALUE obj);
    void  (*mark_movable)(void *objspace, VALUE obj);
    void  (*start)(void *objspace, ...);   /* trigger a GC */
    bool  (*during_gc_p)(void *objspace);
    /* + dozens more */
} rb_gc_impl_t;

gc.c holds a global rb_gc_impl pointer. The build glues in the chosen implementation:

  • Default: gc/default/gc.c is compiled in directly.
  • --with-gc=mmtk: gc/mmtk/mmtk.c calls into a Rust crate bundled separately.

The contract is intentionally narrow — most of the rest of CRuby uses higher-level APIs (rb_gc_mark, RB_OBJ_WRITE, rb_obj_alloc) that route through gc.c.

Default GC algorithm

The in-tree collector (gc/default/gc.c) is mark-sweep with three optional layers:

  1. Generational (rgengc.h/rgengc.c-style code, now inlined into gc/default/gc.c): objects start "young"; surviving a GC cycle promotes them to "old". Most cycles only mark from the young set, scanning the rest of the heap only on rare full GCs.
  2. Incremental: marking is broken into chunks, with mutator code running between chunks. A write barrier records pointers from old to young objects so incremental marking doesn't lose track.
  3. Compacting (gc_compact_* family): on demand (GC.compact), live objects are slid towards the start of each heap page and the old slots reclaimed, then references to moved objects are forwarded.
graph LR
    alloc["Alloc → newobj_of"] --> young[Young / eden]
    young -->|"survives N cycles"| old[Old / tenured]
    young -->|"unreferenced after sweep"| free[Free slot]
    old -->|"unreferenced after major GC"| free
    free -->|"new alloc"| young
    old -->|"GC.compact"| moved[Compacted slot]

Write barriers

Whenever a Ruby object stores a pointer to another Ruby object, code must use a barrier macro:

RB_OBJ_WRITE(parent, &parent->field, child);   /* preferred */
RB_OBJ_WRITTEN(parent, oldval, child);         /* if you've already written */

The macro updates a "remembered set" so a young child of an old parent gets marked even on a young-only GC cycle. Forgetting a write barrier leads to "use-after-free of a young object". Most CRuby type wrappers declare RUBY_TYPED_WB_PROTECTED to advertise that they always use barriers.

Heap pages

Objects live in fixed-size heap pages (HEAP_PAGE_SIZE, typically 16 KB). Each page holds T_OBJECT/T_ARRAY/etc. slots of the same size class — there are several size classes (heap_pages_size) so small and large objects don't share fragmentation.

Free slots within a page form a free list. Allocation pops from the free list; sweep rebuilds it.

Triggering GC

GC runs:

  • When allocation fails because no slot is free.
  • When the heap exceeds tunable thresholds (RUBY_GC_HEAP_GROWTH_FACTOR, etc.).
  • When user code calls GC.start.
  • During shutdown if RUBY_FREE_AT_EXIT=1.

Control variables (mostly read at startup, see gc/default/gc.c::ruby_gc_set_params):

Env var Meaning
RUBY_GC_HEAP_INIT_SLOTS Initial slot budget per page size
RUBY_GC_HEAP_FREE_SLOTS Minimum free slots after a GC
RUBY_GC_HEAP_GROWTH_FACTOR How aggressively to grow on miss
RUBY_GC_OLDMALLOC_LIMIT Bytes of malloc-tracked work that triggers a major GC
RUBY_GC_MALLOC_LIMIT Bytes of malloc-tracked work that triggers a minor GC
RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT Cap on un-protected old objects

GC.stat returns the current values of these knobs and the live counters.

Compaction

GC.compact triggers a compacting cycle:

  1. Mark phase computes the forwarding map.
  2. References are updated through the update_references callback on each object's rb_data_type_t.
  3. Objects are moved.
  4. References to moved objects are pinned (rb_gc_mark_no_pin) where moving would be unsafe.

C extension authors must implement update_references if their data type holds embedded VALUEs.

GC.auto_compact = true runs compaction during minor GCs.

Marking and roots

rb_gc_mark(obj) is the universal "this VALUE is alive" call. Roots include:

  • The VM stack (every rb_control_frame_t's locals and operand stack).
  • Per-Ractor and per-thread state (current CFP, EC, finalizer list).
  • Global symbol table, constant table, and class hierarchy.
  • Encodings (encoding.c).
  • Registered C-level roots (rb_gc_register_address, rb_gc_register_mark_object).
  • Active iseqs, methods, and call caches.

Each typed-data object provides a dmark callback that calls rb_gc_mark on each contained VALUE.

MMTk binding

gc/mmtk/ plugs MMTk (Memory Management Toolkit, https://www.mmtk.io) into the same rb_gc_impl_s interface. MMTk is a Rust framework that provides multiple GC algorithms (Immix, MarkSweep, GenImmix). Building Ruby with --with-gc=mmtk swaps the default GC for an MMTk-driven one.

This is primarily a research and validation tool: bugs that reproduce only under MMTk usually point at missing write barriers in the default GC's clients. CI runs the test suite with MMTk.

spec/.excludes-mmtk/ lists specs that don't apply to MMTk (e.g., specs that assume specific generational behaviour).

Object lifecycle

sequenceDiagram
    participant App
    participant rb_class
    participant gc.c
    participant default_gc
    App->>rb_class: rb_obj_alloc(klass)
    rb_class->>gc.c: rb_newobj_of(klass, flags, ...)
    gc.c->>default_gc: newobj_of()
    default_gc-->>gc.c: VALUE
    gc.c-->>rb_class: VALUE
    rb_class-->>App: VALUE
    App->>App: use VALUE; eventually drops ref
    Note over default_gc: later, on next GC cycle
    default_gc->>default_gc: mark from roots
    default_gc->>default_gc: sweep -> reclaim slot

finalize_list records ObjectSpace.define_finalizer callbacks, which run after sweep finds the object dead.

Inspection

ObjectSpace::dump_all (in ext/objspace/) dumps the live heap as JSON-lines:

require 'objspace'
File.open('/tmp/heap.json', 'w') { |f| ObjectSpace.dump_all(output: f) }

Each line includes the object's address, type, class, memsize, references, file/line of allocation (when ObjectSpace.trace_object_allocations is enabled), and shape id.

GC::Profiler (GC::Profiler.enable; ...; GC::Profiler.report) prints per-cycle stats.

Entry points for modification

  • Algorithm changes: gc/default/gc.c is the place. Most knobs are state in rb_objspace_t; major routines are gc_marks, gc_sweep, gc_marks_finish, gc_compact_move.
  • Adding a typed-data wrapper: define rb_data_type_t with dmark/dfree/dsize/update_references and use TypedData_Make_Struct. See ractor.c for a thoroughly correct example.
  • Hooking GC events: gc/gc.h exposes hook macros; vm_trace.c uses them for :gc_start/:gc_end_mark/:gc_end_sweep trace events.
  • Tuning: introduce a new RUBY_GC_* env var by extending ruby_gc_set_params in gc/default/gc.c.

See systems/threading.md for how the GC interacts with the GVL, and reference/configuration.md for the full env-var list.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Garbage collector – Ruby wiki | Factory