Memory Allocator, PyObject, and Garbage Collection
Memory Allocator, PyObject, and Garbage Collection
After reading this chapter, you will master CPython’s memory architecture: inspect PyObject headers (ob_refcnt, ob_type), understand pymalloc arenas and pools, analyze reference counting deallocation, diagnose circular reference leaks with the gc module, and prevent caching memory leaks using weakref.
Mental model
In CPython, every object on the heap begins with a common C struct header (PyObject):
CPython PyObject Memory Header:
┌────────────────────────────────────────────────────────┐
│ ob_refcnt (64-bit integer tracking active references) │
│ ob_type (Pointer to type object: e.g. &PyLong_Type) │
├────────────────────────────────────────────────────────┤
│ Object-specific payload (e.g. integer digits, chars) │
└────────────────────────────────────────────────────────┘
CPython uses two complementary memory reclamation systems: 1. Reference Counting: Primary deallocation mechanism. The instant ob_refcnt drops to zero, the object’s destructor is called and memory is reclaimed immediately with zero latency. 2. Generational Cyclic Garbage Collector (gc): Runs periodically in the background to detect and break isolated reference cycles (A -> B -> A) that reference counting cannot reclaim.
Pymalloc Allocator Hierarchy:
System Virtual Memory (malloc)
│
▼ 256 KB Arenas
[ Arena 1 ] [ Arena 2 ] [ Arena 3 ]
│
▼ 4 KB Pools (Dedicated to specific size classes: 8, 16, 24, ... 512 bytes)
[ Pool 0 ] [ Pool 1 ] [ Pool 2 ]
│
▼ Blocks (Individual object memory slots)
[ Blk ] [ Blk ] [ Blk ]
Minimal example
Save as memory_gc_showcase.py:
# memory_gc_showcase.py
import gc
import sys
class ClusterNode:
def __init__(self, name: str) -> None:
self.name = name
self.peer: "ClusterNode | None" = None
def test_circular_reference() -> None:
# Temporarily disable automatic GC to observe cyclic leak
gc.disable()
# Create two nodes that reference each other (Circular Reference)
node1 = ClusterNode("alpha")
node2 = ClusterNode("beta")
node1.peer = node2
node2.peer = node1
# Remove stack references; reference count stays at 1 due to the cycle!
del node1
del node2
print("Stack references deleted, but cyclic objects remain in heap memory.")
uncollected_before = gc.collect()
print(f"Manual gc.collect() identified and reclaimed: {uncollected_before} unreachable cyclic objects.")
gc.enable()
def main() -> None:
test_circular_reference()
if __name__ == "__main__":
main()Run via uv run python memory_gc_showcase.py:
Stack references deleted, but cyclic objects remain in heap memory.
Manual gc.collect() identified and reclaimed: 4 unreachable cyclic objects.
Worked examples
Case 1: Reference Counting Mechanics with sys.getrefcount()
Every time an object is passed into a function, stored in a list, or assigned to a name, ob_refcnt increments. When a reference leaves scope, it decrements:
# refcount_tracer.py
import sys
def trace_reference_counts() -> None:
# Note: sys.getrefcount() temporarily increments refcount by 1 because it receives the object as an argument!
target = ["production_cluster_token"]
print(f"Initial reference count : {sys.getrefcount(target) - 1}")
alias1 = target
print(f"After alias1 = target : {sys.getrefcount(target) - 1}")
registry = [target]
print(f"After adding to list : {sys.getrefcount(target) - 1}")
del alias1
print(f"After del alias1 : {sys.getrefcount(target) - 1}")
registry.clear()
print(f"After clearing list registry : {sys.getrefcount(target) - 1}")
if __name__ == "__main__":
trace_reference_counts()Run:
uv run python refcount_tracer.pyOutput:
Initial reference count : 1
After alias1 = target : 2
After adding to list : 3
After del alias1 : 2
After clearing list registry : 1
Case 2: Leak-Free Caching with weakref.WeakValueDictionary
Standard dictionaries keep strong references to cached objects, preventing the garbage collector from ever freeing them. A WeakValueDictionary discards cache entries automatically the moment no other strong references exist:
# weakref_cache.py
import weakref
class ExpensiveSession:
def __init__(self, session_id: str) -> None:
self.session_id = session_id
def main() -> None:
cache = weakref.WeakValueDictionary()
# Create active session
sess = ExpensiveSession("sess_9901")
cache["sess_9901"] = sess
print("Session cached. Is in cache?", "sess_9901" in cache)
print("Cached value:", cache.get("sess_9901"))
# When the strong reference leaves scope or is deleted:
del sess
print("\nStrong reference deleted.")
print("Is session still in WeakValueDictionary?", "sess_9901" in cache)
if __name__ == "__main__":
main()Run:
uv run python weakref_cache.pyOutput:
Session cached. Is in cache? True
Cached value: <__main__.ExpensiveSession object at ...>
Strong reference deleted.
Is session still in WeakValueDictionary? False
Pitfalls
Pitfall 1: Believing del Immediately Frees OS RAM
The del statement does not free memory directly; it only removes a name binding and decrements the object’s reference count. Furthermore, even when an object is destroyed, CPython’s pymalloc allocator typically keeps the 4KB pool allocated to reuse for future Python objects rather than releasing it back to the kernel.
Pitfall 2: Disabling the Garbage Collector in Long-Running Daemons
While disabling gc (gc.disable()) can improve performance in short-lived CLI batch scripts by skipping cyclic scans, doing so in long-running services (FastAPI, Celery workers) causes gradual memory leaks whenever circular references are formed.
Exercises
- Create a doubly linked list node class with
prevandnextpointers, form a cycle, and verify withgc.garbagethat Python’s cyclic collector identifies it. - Use
gc.get_threshold()to inspect the collection thresholds for generation 0, 1, and 2. - Implement an object pool cache using
weakref.refand demonstrate registering a finalizer callback viaweakref.finalize. - Measure the time required to allocate and deallocate 1,000,000 small integer objects versus 1,000,000 large dictionary objects.
Further reading
- Python Standard Library:
gcandweakrefmodules. - CPython Internal Documentation:
Objects/obmalloc.c(pymalloc design). - PEP 442: Safe object finalization.