AnyLearn
All lessons
Programmingadvanced

CPython: reference counting and the cyclic GC

How CPython reclaims memory: immediate reference counting for the common case, plus a generational cyclic garbage collector that catches the reference cycles counting cannot. Covers what changes a refcount, generations and thresholds, the gc module, __del__ and weakref, and why a process's memory does not always shrink.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

Reference counting is the primary mechanism

CPython's main memory manager is reference counting. Every object header carries ob_refcnt. Creating a reference increments it; dropping one decrements it. The instant the count hits zero the object is freed immediately: its finalizer (__del__, if any) runs, and the references it held are decremented in turn, which can cascade.

import sys
a = object()
sys.getrefcount(a)   # e.g. 2
b = a
sys.getrefcount(a)   # 3
del b
sys.getrefcount(a)   # 2

The upside is determinism: an object dies as soon as its last reference disappears, so under CPython files and sockets tend to close promptly. The downsides are per-operation overhead (every bind touches a counter) and one structural gap counting cannot close by itself: reference cycles.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. Reference counting is the primary mechanism

CPython's main memory manager is reference counting. Every object header carries ob_refcnt. Creating a reference increments it; dropping one decrements it. The instant the count hits zero the object is freed immediately: its finalizer (__del__, if any) runs, and the references it held are decremented in turn, which can cascade.

import sys
a = object()
sys.getrefcount(a)   # e.g. 2
b = a
sys.getrefcount(a)   # 3
del b
sys.getrefcount(a)   # 2

The upside is determinism: an object dies as soon as its last reference disappears, so under CPython files and sockets tend to close promptly. The downsides are per-operation overhead (every bind touches a counter) and one structural gap counting cannot close by itself: reference cycles.

2. What changes a reference count

A refcount moves whenever a new reference appears or disappears: binding a name, appending to a list, passing an argument, capturing in a closure, storing as an attribute. When any of those leaves scope or is overwritten, the count drops.

import sys
x = [0] * 1000
sys.getrefcount(x)   # 2: the name x, plus getrefcount's own parameter
lst = [x, x, x]      # three new references
sys.getrefcount(x)   # 5

The famous gotcha: sys.getrefcount(obj) always reports one extra, because obj is bound to the function's parameter while the call runs. So do not trust the absolute number, trust the deltas between readings. This plain counter is also why the free-threaded build had to change how counting works: an unsynchronized shared counter does not scale across cores, as the GIL lesson covers.

3. The cycle problem

Reference counting has one blind spot: a reference cycle keeps every object in it alive even when nothing outside can reach it.

a = {}
b = {}
a['b'] = b     # a refers to b
b['a'] = a     # b refers to a
del a, b       # names gone, but each dict still holds the other

After del a, b, no name points at either dict, yet each still has a refcount of 1 from the other. Pure reference counting would leak them forever. Cycles are common and often invisible: a tree node with a parent pointer, a closure captured by the object it lives on, a self-referential cache. Something separate has to find and reclaim these, and that something is the cyclic garbage collector layered on top of the counter.

4. The cyclic garbage collector

On top of reference counting sits a cyclic garbage collector whose only job is reclaiming unreachable cycles. It tracks container objects, the ones that can hold references to others: dicts, lists, sets, class instances, and tuples that contain containers. Plain values like ints and strings cannot form cycles, so the collector never tracks them.

To find garbage it runs a trial: for each tracked object it copies the refcount, then subtracts the references that come from within the tracked set. Anything still above zero is reachable from outside the set and is kept; anything driven to zero is reachable only through the cycle. Those genuinely unreachable objects are then collected. This is a reachability pass, not counting, which is exactly why it can break cycles that reference counting alone cannot.

5. Generations and thresholds

Scanning every container on every collection would be costly, so the collector is generational, built on the observation that most objects die young. There are three generations: 0, 1, and 2. New tracked objects start in generation 0; each object that survives a collection is promoted to the next.

The default thresholds are (700, 10, 10):

  • Generation 0 is collected once there are about 700 more tracked allocations than deallocations.
  • Generation 1 is collected every 10th generation-0 collection.
  • Generation 2, the oldest and most expensive, every 10th generation-1 collection.

So short-lived objects are checked often and cheaply, while long-lived ones are scanned rarely. gc.get_threshold() returns the numbers and gc.get_count() the running tallies since the last collection of each generation.

6. Driving the collector: the gc module

The gc module exposes the collector. You rarely need it, but a few calls matter for diagnosis and tuning.

import gc
gc.collect()          # force a full collection now; returns count freed
gc.get_threshold()    # (700, 10, 10)
gc.get_count()        # current (gen0, gen1, gen2) tallies
gc.disable()          # stop automatic cyclic collection
gc.get_objects()      # list every object the collector tracks

Disabling the collector is a real, if sharp, tactic: a service that builds one huge, long-lived structure at startup can call gc.disable() to avoid repeatedly scanning objects that will never become garbage, trading the risk of a leaked cycle for lower and steadier latency. Reference counting keeps running regardless, so ordinary non-cyclic garbage is still freed promptly; only cycle detection stops.

7. __del__, resurrection, and weakref

__del__ is a finalizer, run when an object is about to be freed. It is a poor cleanup tool: with pure counting it runs promptly, but for a cyclic object it runs only when the collector reaches the cycle, and its timing is not guaranteed across implementations. A finalizer can even resurrect the object by storing a fresh reference to self, which is legal but confusing.

When you need a reference that does not keep an object alive, use weakref.

import weakref
class Node: pass
n = Node()
r = weakref.ref(n)
r() is n     # True while n is alive
del n
r()          # None: the referent is gone

Weak references let caches, observer lists, and parent pointers avoid creating the very cycles the collector would otherwise clean up. Prefer them to __del__ for cleanup, and use a with block for deterministic resource release.

8. The allocator: why memory does not shrink

Underneath, CPython does not call the system malloc for every small object. pymalloc manages a hierarchy tuned for Python's many tiny, short-lived allocations.

levelsizeholds
arena256 KBmany pools
pool4 KBblocks of one size class
block8 to 512 bytesone small object

Requests up to 512 bytes are carved from pools by size class; larger ones go straight to the system allocator. The key consequence: freeing an object returns its block to the pool for reuse, but an arena is handed back to the OS only when it becomes entirely empty. So a process that briefly built a huge structure can keep a high resident size long after freeing it, even though Python considers that memory available. That is fragmentation, not a leak: later Python allocations reuse it, they just do not return it to the OS.

9. How memory is reclaimed

The two layers working together: reference counting frees most objects instantly, and the cyclic collector sweeps up the unreachable cycles counting leaves behind.

flowchart TD
  A["object loses a reference"] --> B["reference count decremented"]
  B --> C["count reached zero?"]
  C --> D["yes: free now and run finalizer"]
  C --> E["no: object stays alive"]
  E --> F["only reachable through a cycle?"]
  F --> G["cyclic GC detects it in a generation scan"]
  G --> D

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. When is a purely reference-counted object (in no cycle) deallocated?
    • At the next call to gc.collect()
    • The instant its reference count reaches zero
    • Only when the process exits
    • After it survives three GC generations
  2. Two dicts reference each other and every outside name is deleted. What reclaims them?
    • Reference counting, once it catches up
    • The operating system when the process idles
    • The cyclic garbage collector
    • Nothing; they leak for the life of the process
  3. In the default GC thresholds (700, 10, 10), what does the 700 control?
    • Generation 0 is collected after roughly 700 net tracked allocations
    • Objects must survive 700 collections before promotion
    • The collector caps tracked objects at 700
    • Collections run every 700 milliseconds
  4. Why does `sys.getrefcount(obj)` report one more than you might expect?
    • It also counts weak references to obj
    • Reference counts always start at one
    • The GC adds a temporary reference during the call
    • obj is bound to getrefcount's own parameter while the call runs
  5. A service builds a huge structure, frees it, yet resident memory stays high. Most likely cause?
    • A leaked reference cycle the GC missed
    • pymalloc returns an arena to the OS only when it is entirely empty, so freed blocks stay reserved
    • The garbage collector was disabled
    • A __del__ finalizer resurrected the objects

Related lessons