Skip to main content

Python/gc.c (part 2)

Source:

cpython 3.14 @ ab2d84fe1023/Python/gc.c

This annotation covers the collection algorithm's inner phases. See python_gc_detail for generation management, gc_collect, and the public API.

Map

LinesSymbolRole
1-100move_unreachableMove objects with zero adjusted refcount to unreachable set
101-200move_legacy_finalizersMove objects with __del__ to finalizers list
201-350handle_weakrefsNullify or queue weak references to dying objects
351-500finalize_garbageCall __del__ on finalizable objects
501-650collect_with_callbackWrap collect with gc.callbacks calls
651-800deduce_unreachableCompute reachable set by reference graph traversal
801-1000gc_list_* helpersDoubly-linked GC list manipulation

Reading

deduce_unreachable

// CPython: Python/gc.c:680 deduce_unreachable
static void
deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable)
{
/* Phase 1: Subtract internal references.
For each object, call tp_traverse and decrement gc_refs
for each referent in the base generation. */
subtract_refs(base);
/* Phase 2: Objects with gc_refs > 0 are reachable from outside.
Move them (and everything they can reach) to a reachable set. */
move_unreachable(base, unreachable);
}

move_unreachable

// CPython: Python/gc.c:82 move_unreachable
static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
PyGC_Head *gc = GC_NEXT(young);
while (gc != young) {
PyGC_Head *next = GC_NEXT(gc);
if (gc_get_refs(gc) != 0) {
/* Still reachable — traverse to propagate reachability */
PyObject *op = FROM_GC(gc);
traverseproc traverse = Py_TYPE(op)->tp_traverse;
(void) traverse(op,
(visitproc)visit_reachable,
(void *)young);
gc_set_refs(gc, 1); /* mark as definitely reachable */
} else {
/* Zero refs — tentatively unreachable */
gc_list_move(gc, unreachable);
gc_set_refs(gc, GC_UNREACHABLE);
}
gc = next;
}
}

Objects with gc_refs == 0 after subtract_refs are placed in unreachable. visit_reachable is called during traversal to rescue objects that are still reachable via non-tracked references.

handle_weakrefs

// CPython: Python/gc.c:280 handle_weakrefs
static int
handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old)
{
/* For each unreachable object with weak references:
1. Nullify the weakref (set wr_object to Py_None).
2. If the weakref has a callback, move it to a pending list.
3. Call the callbacks (after finalizers, while the objects still exist). */
...
}

Weak reference callbacks are called after finalizers but before the referents are deallocated.

finalize_garbage

// CPython: Python/gc.c:390 finalize_garbage
static void
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
{
/* Call tp_finalize (i.e. __del__) on objects that have one.
After finalization an object may resurrect itself by creating
a new reference — survivors are moved out of the collectable list. */
destructor finalize;
PyGC_Head *gc = GC_NEXT(collectable);
while (gc != collectable) {
PyObject *op = FROM_GC(gc);
PyGC_Head *next = GC_NEXT(gc);
if (!_PyGCHead_FINALIZED(gc) &&
(finalize = Py_TYPE(op)->tp_finalize) != NULL) {
_PyGCHead_SET_FINALIZED(gc);
Py_INCREF(op); /* keep alive during finalization */
finalize(op);
Py_DECREF(op);
}
gc = next;
}
}

Resurrection is handled by move_legacy_finalizers_reachable which re-checks refcounts after all __del__ calls complete.

collect_with_callback

// CPython: Python/gc.c:580 collect_with_callback
static Py_ssize_t
collect_with_callback(PyThreadState *tstate, int generation)
{
/* Fire gc.callbacks before and after collection */
invoke_gc_callback(tstate, "start", generation, 0, 0);
Py_ssize_t result = collect(tstate, generation, &collected, &uncollectable, 0);
invoke_gc_callback(tstate, "stop", generation, collected, uncollectable);
return result;
}

gc.callbacks is a list of callables set up with gc.callbacks.append(cb). Each receives (phase, info) where phase is "start" or "stop".

gopy notes

gopy relies on the Go GC for cycle collection. The GC phases described here have no direct equivalent. gc.collect() in gopy calls runtime.GC(). gc.callbacks is supported as a Python list but the callbacks receive synthetic info dicts since Go's GC does not report collected/uncollectable counts.