Object Model, Reference Counting & Generational GC

Memory management in CPython combines primary Reference Counting with a secondary Generational Cyclic Garbage Collector (GC). Understanding the PyObject header struct, reference count mutations (Py_INCREF / Py_DECREF), cyclic reference deadlocks, and Generational GC sweeps (Gen 0, Gen 1, Gen 2) is fundamental to preventing memory leaks in large-scale Python systems.

This chapter details the PyObject C header struct, Reference Counting mechanics, cyclic reference memory leaks, and CPython’s Generational GC algorithm.


1. The PyObject C Struct Header

Every Python object on the heap starts with a standard C struct header defined in object.h:

CPython PyObject C Header Struct:

[ PyObject @ Heap Address 0x7F9A ]
β”œβ”€β”€ ob_refcnt (uint64_t / 8 Bytes): Live reference count
└── ob_type   (PyTypeObject* / 8 Bytes): Pointer to object's Type struct
[ Object Payload Data (e.g. integer value, string bytes, array pointers) ]
  • ob_refcnt: Stores the exact number of active references pointing to this object.
  • ob_type: Pointer to the PyTypeObject defining the object’s class behavior and C-slots.

2. Primary Memory Management: Reference Counting

CPython tracks memory allocations deterministically using Reference Counting:

  • Py_INCREF(op): Increments ob_refcnt by 1 whenever an object is assigned to a variable, passed to a function, or inserted into a list.
  • Py_DECREF(op): Decrements ob_refcnt by 1 whenever a reference goes out of scope (del, function return, list item removal).

Deterministic Deallocation: The exact instant ob_refcnt drops to 0, CPython immediately deallocates the object’s memory and frees its memory block back to the memory allocator!

import sys

x = [1, 2, 3]
print(sys.getrefcount(x))  # Prints: 2 (x + sys.getrefcount temporary arg reference!)

y = x
print(sys.getrefcount(x))  # Prints: 3 (x + y + temporary arg reference!)

del y
print(sys.getrefcount(x))  # Prints: 2

3. The Cyclic Reference Flaw & Generational GC

Reference counting has one critical failure mode: Cyclic References.

Cyclic Reference Memory Deadlock:

[ Object A ] (ob_refcnt = 1) ─── References ───> [ Object B ] (ob_refcnt = 1)
     ^                                                 |
     └───────────────── References β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

(If local variable pointers to A and B are deleted, both ob_refcnt remain 1!
 Reference Counting CANNOT deallocate them! Memory LEAK occurs!)

Generational Garbage Collector (GC):

To clean up un-reachable cyclic reference islands, CPython runs an asynchronous Generational Cyclic Garbage Collector (gc module):

  • Generation 0 (Gen 0): Youngest objects. Scanned frequently (every 700 allocations).
  • Generation 1 (Gen 1): Intermediate objects that survived a Gen 0 GC collection sweep.
  • Generation 2 (Gen 2): Long-lived objects (survived Gen 1 collection). Scanned infrequently.

Tri-Color Marking Algorithm:

The GC detects cycles by subtracting internal reference counts across doubly-linked lists of container objects (PyGC_Head). If an object’s reference count drops to zero after subtracting internal references within the collection group, it is identified as unreachable cyclic garbage and freed.


4. Production Trade-offs & gc.disable()

  • gc.disable() for High-Throughput Batch Jobs: In multi-process workers (like Gunicorn or Celery), running full Gen 2 GC collections during execution can cause Stop-The-World pause latency spikes. Disabling GC (gc.disable()) or tuning thresholds (gc.set_threshold()) during request execution improves latency if code avoids cyclic references.
Display Options
Appearance
Text Size
100%