In a reference-counted system, which events change an object's count, and what happens the moment that count reaches zero?
answer
- one integer per object
- owners are counted, readers are not
- copy raises it, drop lowers it
- overwrite: increment new before decrement old
- zero releases the object's own handles too
basics
~20 sEvery new owning handle to an object increments its count; every handle dropped or overwritten decrements it. At zero the object's cleanup runs, every handle it holds is released in turn, and its memory is reclaimed.
solid answer
~40 sThe count is one integer per object recording how many **owning handles** currently exist — a local variable, a field, a container slot. It starts at 1 for the creating handle, goes up whenever a handle is copied into another location, and goes down whenever a handle is destroyed, cleared or overwritten. Reading through a handle changes nothing: the count tracks ownership, not use. Overwriting is two updates in a fixed order — increment the new target, then decrement the old one, so that assigning a handle to itself cannot free the object mid-way. Reaching zero proves no owner is left, so the object is destroyed there and then: cleanup runs, each handle it holds is released (which may destroy more objects, recursively), and the memory goes back to the allocator.
code
pseudocode · 14 linesrelease(obj):
obj.count = obj.count - 1
if obj.count > 0:
return // someone still owns it
run_cleanup(obj) // fields still valid here
for each field f in obj holding a handle:
release(f.target) // may cascade further
free_memory(obj)
assign(slot, new_target):
increment(new_target) // before, never after
old = slot
slot = new_target
release(old) // safe even if old == new_targetgo deeper
Remember the two directions: a new owning handle adds one, a handle that dies subtracts one, and zero means the object goes away. Reading an object never changes its count.
Be able to walk an overwrite as three steps in order, name why the increment comes first, and explain that destruction at zero releases the destroyed object's own handles recursively.
Show that the cost of one release is unbounded in what the object exclusively owns, and that the whole scheme is one invariant — count equals live owners — that every elision must preserve.
Frame it as a contract between compiler and runtime: who is obliged to emit each update, which elisions are provably safe, and what the frequency of those updates costs across a whole codebase.
## What the count actually counts An object's reference count is a single integer, stored with the object or beside it, recording **how many owning handles currently exist** for that object. An owning handle is any location that has taken responsibility for keeping the object alive: a local variable, a field of another object, a slot in a container, an entry in a registry. The count is not how many times the object has been read, not how many places once mentioned it, and not how many objects the object itself points to. The invariant the whole scheme rests on is simple and unforgiving: **the count equals the number of live owning handles, at every moment**. Every operation that creates an owning handle increments exactly once; every operation that destroys one decrements exactly once. Miss an increment and the object is freed while someone still holds it — a use-after-free. Miss a decrement and the object is never freed — a leak. There is no third outcome, and nothing scans the heap later to notice the mistake, which is why counted systems put the updates in generated code or in a handle type's copy-and-destroy hooks rather than trusting a human to write them. ## The events that move it | event | effect on the count | |---|---| | object created | starts at 1, owned by the creating handle | | handle copied into a variable, field or container | +1 | | handle destroyed: scope exit, container cleared, owner destroyed | -1 | | handle overwritten with another | +1 on the new target, then -1 on the old | | field read or object dereferenced through an existing handle | no change | | ownership transferred, source left empty (a move) | no change | Two rows carry most of the misunderstanding. Reading costs nothing, because ownership and use are different things. And a **move** — a transfer where the source is left holding nothing — deliberately performs neither update, because the number of owning handles has not changed. Moves are how counted code avoids paying for ownership that merely changes address. ## Overwriting: increment before decrement An assignment of one handle over another is two count updates, and their order is not a free choice. A correct implementation does: 1. increment the count of the object the right-hand handle refers to; 2. store the new value into the destination; 3. decrement the count of the object the destination used to refer to. Reverse steps 1 and 3 and self-assignment becomes fatal. If both sides name the same object — directly, through an alias, or through two container slots that happen to hold the same thing — decrementing first can drop the count to zero, destroy the object, and leave the following increment writing into freed memory. The bug is invisible in ordinary testing, because the two sides are usually different objects. ## What zero triggers Zero is not a hint that the object is probably dead; it is proof that no owning handle exists. In the basic scheme the object is therefore destroyed **immediately, inside whichever execution context performed the decrement**, in this order: 1. the object's own cleanup runs, while its fields are still valid; 2. every handle the object holds is released — each of those decrements may itself reach zero and destroy another object, recursively; 3. the memory is returned to the allocator. Step 2 is the step candidates forget. Destroying one object is really destroying a whole subgraph: whatever that object exclusively owns dies with it, in one unbroken burst of work charged to the code that happened to drop the last handle. That is why the cost of a single release is unbounded in the size of what the object exclusively owns, and why a naive recursive release is usually replaced by an explicit worklist. ## Where the model holds, and where it does not Immediacy is the selling point. A resource is released at the exact program point where its last owner dies, so descriptors, sockets and buffers close predictably with no collection cycle and no finalisation queue. The costs are equally structural: - the updates are frequent, and ordinary code that only copies handles pays for them on paths that do nothing else; - a count occupies space in or beside every counted object; - a group of objects that hold handles to each other keeps its own counts above zero, so counting alone never reclaims such a group — a limitation the scheme is defined to have and that other mechanisms exist to address. Ecosystems differ in how much of the update traffic they elide: some prove statically that one handle's lifetime is contained inside another's and drop both updates, others emit every one. What never differs is the invariant. Every legal optimisation is legal only because it preserves *count equals live owning handles*.
- What breaks in an implementation that decrements the old target before incrementing the new one?Self-assignment. If both sides refer to the same object and its count is 1, the decrement reaches zero, the object is destroyed and its memory returned, and the increment that was meant to keep it alive then writes into freed memory. Incrementing first makes the count temporarily 2, so the following decrement is harmless. The aliasing need not be visible in the source: two container slots holding the same object are enough.
- Why does a newly created object start at 1 rather than 0?Because the code that created it is already an owner. Starting at 1 means the creating handle's eventual release is the ordinary decrement that can reach zero. A scheme that started at 0 would leave a window in which any release, or any transient copy-and-drop, would destroy an object nobody had finished building — so such schemes require the creator to increment before the object may be stored anywhere.
- If an object's count falls from 3 to 2, what work does the implementation do?Only the subtraction and the zero test. Nothing is cleaned up, no memory moves, and no other object is touched, because the object still has owners. This is why the common path of counting is cheap in work but frequent in occurrence: the interesting work happens only on the one decrement in the object's life that reaches zero.
A hotel front desk counts key cards issued for a room, not people currently inside it. The room is stripped and reassigned when the last card comes back, and every card handed back is one fewer claim on it.
saying these in an interview costs you the question
- Says dereferencing or reading a field bumps the count
- Decrements the old target before incrementing the new one
- Thinks destroying an object frees only that object's own bytes
- Confuses the count with how many handles the object itself holds
- Claims a transfer of ownership must increment and then decrement