skip to content

questions

5

In a reference-counted system, which events change an object's count, and what happens the moment that count reaches zero?

level: middleimportance: must knowfreq 62%

answer

  1. one integer per object
  2. owners are counted, readers are not
  3. copy raises it, drop lowers it
  4. overwrite: increment new before decrement old
  5. zero releases the object's own handles too

basics

~20 s

Every 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 s

The 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 lines
pseudocode
release(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_target

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

A million-node counted chain loses its only head handle and every node is freed at once, so why can that hurt tail latency?

level: seniorimportance: must knowfreq 55%

basics

~20 s

One decrement can destroy a whole structure. The thread that dropped the head handle runs a million cleanups, decrements and frees inline, in the middle of whatever request it was serving, so a cheap-looking assignment becomes an unbounded pause.

open as a page

Why is it unsafe to release an owning handle to an object before the last read of that object's fields?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Releasing may take the count to zero, which destroys the object immediately. Any pointer still aimed at the object or inside it — an interior pointer, a borrowed field address, a buffer handed to someone else — becomes a dangling pointer at that instant, and the next read is a use-after-free.

open as a page

When designing a counted object representation, would you store each object's count in its header or in a side table?

level: principalimportance: should knowfreq 38%

basics

~20 s

A header count is one load and store on a line the code already touched, but every object pays the space and every update dirties the object's own page. A side table keeps objects clean and costs a lookup per update. Object size distribution and page sharing decide it.

open as a page

What can a reference-counting implementation do when an object's narrow count field cannot hold another increment?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Three safe answers exist: saturate the count so the object becomes immortal, spill it into a wider side entry, or treat the overflow as a fatal error. Wrapping is not an option — a count that wraps to zero frees an object that still has owners.

open as a page