Python Garbage Collection and Circular References
Python manages memory and tracks object lifetimes primarily through reference counting, supplemented by a generational cyclic garbage collector. While reference counting immediately frees objects whose reference counters fall to zero, it cannot resolve circular references on its own. To prevent memory leaks caused by isolated reference cycles, CPython utilizes an auxiliary garbage collection algorithm that tracks container objects across three generations, isolates unreachable groups by simulating reference decrements, and frees cyclical memory structures.
Tracking Object Lifetimes: Reference Counting
At the core of CPython's memory management is reference counting.
Every Python object contains a base structure (PyObject)
that includes an internal field called ob_refcnt.
- Increments: When an object is instantiated,
assigned to a variable, passed to a function, or appended to a
container, its
ob_refcntincreases by one. - Decrements: When a variable goes out of scope, is
reassigned, or explicitly deleted with
del, itsob_refcntdecreases by one. - Immediate Deallocation: As soon as an object's reference counter hits zero, its memory is instantly freed, along with any resources it holds.
While deterministic and efficient for the vast majority of operations, reference counting fails when two or more objects reference each other, creating an isolated cycle.
The Problem of Circular References
A circular reference occurs when objects directly or indirectly hold references to one another. For example, if object A references object B, and object B references object A, both maintain a reference count of at least one. If all outside references to A and B are deleted, both objects become inaccessible to the running program, but their reference counts never drop to zero. As a result, standard reference counting cannot reclaim their memory.
Detecting Cyclic References
To resolve reference cycles, Python runs an active cyclic garbage collection algorithm designed specifically to inspect container objects—such as lists, dictionaries, sets, tuples, and custom class instances. Atomic types like integers, strings, and floats cannot hold references to other objects and are therefore ignored by this system.
Python tracks container objects by linking them together into doubly linked lists. When a garbage collection run begins, the detector performs the following steps:
- Copying Reference Counts: The collector copies each
container's reference count into a dedicated internal field
(
gc_refs). - Subtracting Internal References: The collector
iterates through the tracked container objects and inspects every object
they point to. For each internal reference found within the tracked set,
the collector decrements the target object's
gc_refs. - Isolating Cycles: After all internal links are
accounted for, objects with
gc_refs > 0are deemed reachable from outside the isolated set (such as from global variables or the active execution stack). - Rescuing Reachable Chains: Any object marked as reachable also marks all objects it points to as reachable, propagating until no more reachable objects can be found.
- Reclaiming Garbage: Any remaining objects that
still have a
gc_refsof zero are confirmed to be members of an isolated, unreachable reference cycle. The collector clears their references, drops their true reference counts to zero, and triggers deallocation.
Generational Garbage Collection
Scanning every container object on every garbage collection pass would cause severe performance bottlenecks. Python optimizes this process using the weak generational hypothesis: most allocated objects have short lifespans and die shortly after creation.
The cyclic garbage collector organizes tracked containers into three generations:
- Generation 0: Newly created container objects are placed here. This generation is inspected frequently.
- Generation 1: Objects that survive a Generation 0 collection are promoted to Generation 1, which is scanned less often.
- Generation 2: Objects that survive a Generation 1 collection are promoted to Generation 2, representing long-lived objects (such as modules and long-running services). This generation is scanned the least frequently.
Collection Triggers and Thresholds
The garbage collector triggers scans based on allocation thresholds rather than fixed time intervals. CPython tracks the net difference between allocations and deallocations.
When the net allocations in Generation 0 exceed a predefined threshold (by default, 700 net allocations), a Generation 0 collection is triggered. Each time Generation 0 is collected, a counter for Generation 1 increments. When that counter exceeds its threshold (defaulting to 10), Generation 0 and Generation 1 are collected together. Similarly, after Generation 1 is collected a set number of times (defaulting to 10), a full collection runs across all three generations.
By confining cyclic detection sweeps primarily to young objects, Python maintains minimal execution overhead while continuously preventing circular references from exhausting system memory.