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.rbon top of C primitives ingc.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.cis compiled in directly. --with-gc=mmtk:gc/mmtk/mmtk.ccalls 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:
- Generational (
rgengc.h/rgengc.c-style code, now inlined intogc/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. - 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.
- 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:
- Mark phase computes the forwarding map.
- References are updated through the
update_referencescallback on each object'srb_data_type_t. - Objects are moved.
- 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 slotfinalize_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.cis the place. Most knobs are state inrb_objspace_t; major routines aregc_marks,gc_sweep,gc_marks_finish,gc_compact_move. - Adding a typed-data wrapper: define
rb_data_type_twithdmark/dfree/dsize/update_referencesand useTypedData_Make_Struct. Seeractor.cfor a thoroughly correct example. - Hooking GC events:
gc/gc.hexposes hook macros;vm_trace.cuses them for:gc_start/:gc_end_mark/:gc_end_sweeptrace events. - Tuning: introduce a new
RUBY_GC_*env var by extendingruby_gc_set_paramsingc/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.