Garbage Collection
A garbage collector frees memory the program can no longer reach, so the programmer never writes a free and can never free anything too early. There are two ways to find what is unreachable. Count the references to every object, or periodically trace everything reachable from the program's roots. They put their costs in opposite places: counting pays a little on every assignment and frees at once, tracing pays nothing per assignment and pays in batches.
CPython uses both. Neither one, in CPython or anywhere else, stops a program from holding memory it will never use again, and that gap is where every leak in a collected language lives.
Reachable, Not Used
A collector asks one question about each object: does a chain of references lead to it from a root? The roots are the places a program can reach without following anything: local variables on each thread's stack, global variables, and the interpreter's own tables. An object with such a chain is live. An object without one is garbage.
No collector can ask the other question, whether the program will ever read the object again. An object that is reachable and never read again is live as far as any collector can know. A dictionary used as a cache that nobody ever trims is perfectly reachable, and it grows until the process runs out of memory.
Reference Counting
With reference counting, every object carries a count of the references to it, raised and lowered on every assignment, every argument passed and every insert into a container. When the count reaches zero, the object is freed on the spot. That makes freeing deterministic: a file object closes the moment its last reference goes, not at some later collection.
It has two costs. Every reference change is a write to memory, even in code that only reads the object. And it cannot free a cycle: two objects that point at each other keep each other's counts above zero forever, even when nothing else can reach either of them.
import gc class Node: pass a, b = Node(), Node() a.other, b.other = b, a # each points at the other del a, b # unreachable, but both counts are still 1 gc.collect() # the cycle collector frees them: returns 2
The listing makes two objects that refer to each other and then deletes both names. Nothing can reach the pair any more, but each still holds a reference to the other, so neither count falls to zero. On CPython 3.15, the pair stayed alive after the delete, and a call to the cycle collector found and freed both, reporting two objects collected.
Tracing
A tracing collector starts from the roots, marks everything it can reach by following references, and reclaims the rest. It reclaims either by sweeping the unmarked blocks back into free lists, or by copying the live objects somewhere compact and releasing the old space whole. The marking is a graph traversal, the same one Chapter 8 teaches.
Nothing is paid per assignment, and cycles are no problem: an unreachable cycle is never marked, so it is reclaimed with everything else. The work comes in batches instead, and a batch that stops the program while it runs is a pause.
Generations
Most objects die young: a request's temporaries, a loop's intermediate results. Collectors exploit this by splitting objects by age, tracing the young ones often and cheaply and the old ones rarely. Java's and .NET's collectors and V8, the JavaScript engine, all rely on it, and so does CPython, for cycles only.
CPython's cycle collector has used three generations for most of its history. CPython 3.14.0 shipped an incremental collector with two generations, young and old, to shorten its longest pauses. After reports of significant memory pressure in production, the 3.14.5 release and Python 3.15 returned to the three-generation collector of 3.13. On 3.15 the collector reports three generations with thresholds of 2,000, 10 and 10. This is the most version-sensitive fact in the chapter, and it is stated with its version for that reason.
Pauses
A collection that stops every thread adds its whole duration to whatever requests are in flight. So garbage collection shows up in tail latency, the 99th percentile, not in the average. Modern tracing collectors do most of their marking while the program keeps running: Go's collector and Java's ZGC keep pauses below a millisecond, while Java's G1 aims for 200 milliseconds by default.
Reference counting has pauses too, in a different place. Dropping the last reference to a dictionary of ten million objects frees all ten million right there, on the thread that dropped it. On CPython 3.15, deleting a dictionary of ten million string values took 0.19 seconds, inside whatever request happened to do it.
Forked Workers and Reachability Leaks
A Python service pays reference counting on every line and a cycle collection now and then. The count updates have a second cost in servers that fork worker processes. The workers share their parent's memory until one of them writes to a page, which then gets copied (Chapter 10). Merely reading an object updates its count, so reading shared data turns shared pages into private copies. The cycle collector does the same on a larger scale: every collection writes into the header of every object it examines, live ones included. Instagram described this in 2017, when disabling the cycle collector bought its Python fleet about 10% more capacity, and CPython 3.7 added a way to move long-lived objects out of the collector's reach before forking.
The leaks that remain are all reachability leaks. An unbounded cache. A module-level list of every request seen. A caching decorator on a method, which stores each instance as part of its cache key and so keeps every instance alive. On CPython 3.15, an instance whose method carried an unbounded least-recently-used cache stayed alive after its last name was deleted and a collection ran.
- "Garbage collection means memory leaks cannot happen." The collector frees only the unreachable. An unbounded dictionary used as a cache, a list of callbacks never removed, and a memoized method holding each instance all stay reachable, and they grow until the container's memory limit kills the process.
- "CPython frees an object as soon as the code stops using it." Reference counting frees at the last reference, not at the last use. A cycle waits for the cycle collector, and a stored exception keeps its traceback's frames, and every local variable in them, alive.
- "Forcing a collection fixes a leak." A forced collection finds only unreachable cycles. A reachability leak survives it untouched, and forcing collections in a loop adds pauses for nothing.
- "Tracing collectors are slow and reference counting is fast." Counting pays on every reference change and cannot collect cycles; tracing with generations often has higher throughput. The costs sit in different places, and neither is free.
- "Turning off Python's garbage collector stops memory from being reclaimed." Only the cycle collector stops. Reference counting keeps freeing nearly everything, which is why large Python deployments, Instagram's among them, have run in production with the cycle collector disabled.
- Bound every cache by size or age. The collector cannot tell a cache from a leak.
- Remove listeners and registry entries when their owner goes away, or hold them through weak references. A weak reference does not keep its target reachable.
- Watch 99th-percentile latency next to the collector's pause measurements. Collection pauses surface in the tail, never in the mean.
- Diagnose a leak by counting live objects per type over time. The type that keeps growing is the leak, and a forced collection only removes what was already garbage.
Knowledge Check
Two Python objects refer to each other, and nothing else refers to either. Under reference counting alone, what happens to them?
- They are freed at once, because no variable in the program names them
- They are freed at the next assignment, when their counts are rechecked
- They stay allocated, because each keeps the other's count at one
- They raise an error, because the interpreter forbids cyclic references
A Java service has a median latency of 8 ms. Where would its garbage-collection pauses show up?
- In the median, which rises by the length of every collection pause
- In the tail, the 99th percentile, on the requests that met a pause
- Nowhere, because a pause only delays the collector and not requests
- In the error rate, because requests that meet a pause always fail
A class uses an unbounded caching decorator on one of its methods. Instances are created per request and never used again. What happens to memory?
- It stays flat, because each instance is freed when the request ends
- It stays flat, because the cycle collector frees the cached instances
- It grows until the next forced collection, which then clears the cache
- It grows with every request, because the cache keeps every instance alive
A team disables CPython's cyclic garbage collector in production. What stops, and what keeps working?
- Everything stops: no object is freed until the process exits
- Cycles are no longer freed; everything else is still freed by counting
- Nothing changes, because the setting only affects debug builds
- Counting stops, but the cycle collector still frees all unreachable objects
A pre-forking web server loads a large read-only table in the parent before forking workers. Why does each worker's memory still grow as it reads the table?
- Each worker reloads the table from disk on its first request
- The operating system never shares memory between processes
- Reading an object updates its count, which copies the shared page
- The cycle collector moves the table to new addresses in each worker
You got correct