Reference Counting and Object Model
Reference Counting and Object Model
CPython frees an object when its reference count hits zero. Counts rise with bindings, arguments, and container membership; they drop when names go away:
import sys
a = [] # count 1
b = a # count 2
sys.getrefcount(a) # reports 3 — the temporary argument counts too
del b # back to 1; object still alive
Values are objects too: integers -5..256 are singletons, and identifier-like strings are interned, so a is b may be True for small integers and short identical identifiers — and False for larger ones created separately. Lists over-allocate capacity so append is amortized O(1), which is why sys.getsizeof exceeds len(items). Memory is a tree of references from stack frames and globals down to leaves.
The Cyclic Collector
The Cyclic Collector
Reference counting cannot free cycles (a.next = b; b.prev = a). A generational mark-and-sweep collector handles that:
import gc
gc.get_threshold() # (700, 10, 10): gen0 threshold, gen1 increment, gen2 increment
gc.collect() # force a full collection; returns unreachable count
gc.is_tracked(obj) # containers are tracked; bare ints/floats usually are not
gc.freeze() # promote import-time objects out of collection scope
Objects start in generation 0; surviving a collection promotes them (0→1→2), so long-lived structures are scanned rarely. del name removes a binding, not an object still referenced elsewhere. weakref registers without holding on — ideal for caches and parent back-pointers, since strong refs there create exactly the cycles the collector must chase.
Measuring and Reducing Memory
Measuring and Reducing Memory
Diagnose before optimizing:
import tracemalloc
tracemalloc.start()
base = tracemalloc.take_snapshot()
run_workload()
for stat in tracemalloc.take_snapshot().compare_to(base, "lineno")[:10]:
print(stat)
Reduction levers, in order of payoff:
- Algorithm: don’t hold the whole dataset — stream with generators
__slots__: drops the per-instance__dict__(and__weakref__unless requested) when instances number in the millions- Arrays over lists:
array('f')or NumPy for numeric data — one machine value per element instead of pointer + PyObject header - Intern repeated strings: enum labels and constant keys reused across rows
- WeakValueDictionary caches whose entries can be collected
Interview angle: "memory growth" is usually retention (an unexpected reference), not leakage — name the tool that proves it.
Quiz
1. Why can’t reference counting alone free a doubly linked list cycle?
2. What does gc.get_threshold() returning (700, 10, 10) mean?
3. Best first step when a service’s RSS climbs steadily?
Flashcards
Question
Reference counting
Click to reveal answer
Answer
CPython’s primary free mechanism; count reaches zero → deallocated. Cycles need the GC.
Question
Generational GC
Click to reveal answer
Answer
Gen0/1/2 mark-and-sweep; survivors promote so old objects are scanned rarely.
Question
sys.getrefcount
Click to reveal answer
Answer
Shows the count — one higher than expected because the call itself holds a reference.
Question
__slots__ benefit
Click to reveal answer
Answer
Replaces per-instance __dict__ with fixed slots, cutting memory when instances are huge in number.
Revision Notes
Key Takeaways
- 1. Reference counting frees acyclic objects instantly; the generational collector breaks cycles
- 2. Small-int/string interning and list over-allocation explain surprising is/getsizeof results
- 3. Measure with tracemalloc, then cut retention: streams, slots, arrays, weakrefs
Interview Tips
- • Give both halves of CPython memory management — refcounts first, then the cyclic GC with generations
- • Frame "leak" investigations as retention hunts and name the snapshot tool you would use
Cheat Sheet
refcount frees acyclic; gen GC (700,10,10) breaks cycles; small ints -5..256 interned; sys.getrefcount (+1); slots/weakref/tracemalloc to cut retention.
Continue Learning
Before You Start
- No prerequisites listed.