Skip to main content

pycore_gc.h — GC internals

Include/internal/pycore_gc.h is the private header that defines the bookkeeping layer the cyclic garbage collector adds to every tracked heap object. Public GC API lives in Include/cpython/objimpl.h; this file is the part you are not supposed to touch from extension code.

Map

LinesSymbolRole
1–30PyGC_HeadTwo-pointer struct prepended to every tracked object
31–55_PyGCHead_PREV / _PyGCHead_NEXTAccessor macros that strip the tag bit from the low pointer
56–80_PyGC_PREV_MASK_FINALIZED / _PyGC_PREV_MASK_COLLECTINGTag-bit constants packed into the low bits of _gc_prev
81–110gc_refs field semanticsVisit counter during mark; flag values GC_REACHABLE, GC_TENTATIVELY_UNREACHABLE
111–145_PyObject_GC_TRACK / _PyObject_GC_UNTRACKInsert/remove an object from the generation 0 list
146–200Generation structs and _PyGC_generationPer-generation doubly-linked list head plus collection statistics

Reading

PyGC_Head: the hidden prefix

Every object allocated with PyObject_GC_New gets a PyGC_Head placed immediately before the visible PyObject header. The two fields form a doubly-linked list that the collector walks during each collection.

// CPython: Include/internal/pycore_gc.h:23 PyGC_Head
typedef struct {
uintptr_t _gc_next;
uintptr_t _gc_prev;
} PyGC_Head;

uintptr_t is used rather than a pointer type so the implementation can pack status bits into the low bits of each word without tripping strict-aliasing rules.

Tag-bit encoding in _gc_prev

The low two bits of _gc_prev carry flags rather than address bits. Bit 0 marks the object as having a __del__ finalizer that has already been called (PREV_MASK_FINALIZED). Bit 1 marks the object as currently inside a collection pass (PREV_MASK_COLLECTING). Real pointer values are recovered by masking those bits off.

// CPython: Include/internal/pycore_gc.h:34 _PyGCHead_PREV
#define _PyGCHead_PREV(g) ((PyGC_Head*)(g)->_gc_prev & _PyGC_PREV_MASK)

// CPython: Include/internal/pycore_gc.h:36 _PyGCHead_SET_PREV
#define _PyGCHead_SET_PREV(g, p) \
do { (g)->_gc_prev = ((g)->_gc_prev & ~_PyGC_PREV_MASK) \
| ((uintptr_t)(p)); } while (0)

The _PyGC_PREV_MASK constant is ~0x3UL, so the two low bits survive the SET macro unchanged while the pointer is written cleanly.

gc_refs and the mark phase

During the mark phase gc_refs is loaded with the object's true reference count. The collector then decrements it once for each reference found within the candidate set. An object whose gc_refs reaches zero after that sweep is only reachable through other candidates, making it tentatively unreachable.

// CPython: Include/internal/pycore_gc.h:92 GC_REACHABLE
#define GC_REACHABLE (-1)
#define GC_TENTATIVELY_UNREACHABLE (-2)

Objects confirmed as live get gc_refs reset to GC_REACHABLE. Objects not rescued by the second pass keep GC_TENTATIVELY_UNREACHABLE and are finalized.

Tracking and untracking

_PyObject_GC_TRACK inserts a freshly constructed object into the generation 0 list. The macro resolves to an inline function in debug builds so it can assert that the object is not already tracked.

// CPython: Include/internal/pycore_gc.h:128 _PyObject_GC_TRACK
static inline void _PyObject_GC_TRACK(PyObject *op) {
PyGC_Head *gc = _Py_AS_GC(op);
// assert not already tracked ...
PyGC_Head *last = (PyGC_Head*)(_PyRuntime.gc.generation0->_gc_prev
& _PyGC_PREV_MASK);
_PyGCHead_SET_NEXT(last, gc);
_PyGCHead_SET_PREV(gc, last);
_PyGCHead_SET_NEXT(gc, _PyRuntime.gc.generation0);
_PyGCHead_SET_PREV(_PyRuntime.gc.generation0, gc);
}

gopy notes

gopy does not port the cyclic GC. Go's own garbage collector handles all heap memory, so PyGC_Head has no direct equivalent. The _PyObject_GC_TRACK / _PyObject_GC_UNTRACK call sites in CPython are omitted or replaced with no-ops when porting allocation paths. If cycle detection for Python-level __del__ ordering becomes necessary in a later milestone, a pure-Go mark-and-sweep over the objects.Object graph is the intended approach rather than a port of this header.

CPython 3.14 changes

CPython 3.14 consolidates per-interpreter GC state that was previously global. _PyRuntime.gc fields moved into PyInterpreterState.gc so that each sub-interpreter runs an independent collection cycle. The _PyGC_generation struct gained a _gc_count field used by the new incremental collector prototype. The tag-bit layout of _gc_prev is unchanged from 3.12.