How Python Takes Out Its Own Garbage
Python manages memory automatically, freeing developers from manual allocation and deallocation. It does this through two complementary mechanisms: reference counting and a generational garbage collector for cyclic references. This article covers how garbage collection works in CPython. In other implementations such as PyPy, it works under a different mechanism Reference Counting: The Primary…
Python automates memory management, eliminating the need for developers to manually allocate and deallocate memory. This is done through two methods: reference counting and a generational garbage collector. The article discusses how garbage collection operates within CPython, the standard Python implementation. Other Python implementations, such as PyPy, utilize a distinct mechanism.
Reference counting serves as the primary method, attributing each object with a reference count that increases when new references are assigned, stored in containers, or passed into functions. Conversely, the count decreases when references go out of scope, are reassigned, or manually deleted using the "del" statement. When the count reaches zero, CPython immediately deallocates the object.
This differs from other garbage-collected languages like Java, JavaScript, or the PyPy implementation, where collection timing is unpredictable. However, reference counting cannot handle cyclic references, where objects reference each other, keeping their counts above zero even when unreachable from the program. To address this issue, Python includes a separate cyclic garbage collector implemented in the "gc" module.
This collector is based on the generational hypothesis, suggesting that most objects become unreachable quickly. Objects are grouped into three generations: 0 (newly created objects, most frequent collection), 1 (survived one collection, less frequent), and 2 (survived multiple collections, least frequent). An object starts in generation 0; if it survives a collection pass, it moves to generation 1, eventually reaching generation 2.
Each generation has a threshold count of allocations that triggers a collection pass, detailed as (700, 10, 10) using the "gc.get_threshold()" function. The cycle-detection process involves tracking container objects like lists, dictionaries, tuples, class instances, and closures - any object capable of holding references to other objects.
Each tracked object carries a struct linking it into a per-generation list. When a generation's allocation-minus-deallocation count surpasses its threshold, a collection pass is executed on that generation: the refcount of each tracked object is copied to a scratch field, and references from other tracked objects are subtracted to eliminate internal cycle references.
Any object with a positive count must be referenced from outside the tracked set, so it is retained. Outward tracing from those objects rescues anything reachable from them, even if it resides within a cycle. Objects still at zero are unreachable and are swept away. Objects surviving a pass move to the next generation, undergoing less frequent checks.
The time complexity of this operation is O(n). Modifying the cyclic collector is generally discouraged, but in performance-sensitive code that avoids creating reference cycles, it can be disabled using the "gc.disable()" function. Turning it back on requires the "gc.enable()" function, and a full collection can be forced via "gc.collect()", returning the count of unreachable objects found.
The current object counts per generation, current thresholds, and adjusting them can be accessed with "gc.get_count()", "gc.get_threshold()", and "gc.set_threshold()", respectively. Developers should be mindful of allocation-heavy loops, as generation-0 collection triggers after a net allocation threshold (default 700), potentially triggering frequent passes in code that repeatedly creates and discards containers.
To reduce frequent gen-0 collections, consider reusing containers instead of creating new ones. In cases of likely cycles, such as parent/child structures, weak references can break the cycle, preventing memory leaks.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.