A long-lived product built on reference counting leaks cycles in production; how do you choose between a cycle collector, a non-owning-edge convention, and a periodic backup trace?
answer
- ask who decides the graph's shape
- convention is a promise nothing checks
- collector work follows recent decrements
- a reachability pass needs exact roots
- pause budget versus steady footprint drift
basics
~20 sDecide by who controls the graph's shape. Author-controlled shapes suit a non-owning-edge convention; graphs shaped by run-time data or plugins need a cycle collector; a backup reachability pass catches everything but demands exact roots and a pause budget.
solid answer
~50 sStart from the graph, not from the mechanism. If every ring is one your own code creates, a non-owning edge on the containment back-edge costs nothing at run time and is the right default — but it is a promise nothing checks, so it decays with team size and fails outright on shapes decided by data or by plugins. A cycle collector removes the dependence on discipline and keeps its work proportional to recent decrements rather than to the heap, charging candidate bookkeeping, several traversals per object and a quiescent point. A periodic reachability pass is the most complete and the least selective: it needs every root enumerated exactly, which is often the very thing counting was chosen to avoid, and it brings back a pause proportional to the live set. Most real systems keep the convention and add one of the two mechanisms behind it.
go deeper
Know that the three answers exist: place a non-owning edge by hand, run a collector that hunts rings, or take an occasional full reachability pass — and that a counted system needs at least one of them.
Name what each charges: the convention is free at run time but unchecked, the collector charges bookkeeping and repeated traversals, the reachability pass charges a pause proportional to the live set.
Anchor the choice in a real system: what decides the graph's shape, what the latency and footprint budgets are, and how you would get evidence that the mechanism you picked is reclaiming the rings.
Own the trade explicitly. Say which currency you are willing to spend, note that root enumeration is a precondition and not a price, and design the signal that tells you the convention has quietly stopped holding.
## The three answers and what each charges | option | what it costs | what it assumes | where it fails | |---|---|---|---| | non-owning edge placed by hand | nothing at run time; a guard at each use of the edge | that a human knew the ring would exist | graphs shaped by run-time data, plugins, or a new contributor | | cycle collector over candidates | bookkeeping per decrement, several traversals per visited object, a quiescent point | counts are exact and the subgraph is still while it runs | a candidate near the top of a huge structure; approximate counts | | periodic reachability pass | a pause proportional to the live set | every root can be enumerated exactly | settings where roots cannot be found precisely at all | The honest framing for an interview is that these charge in **different currencies** — run-time cost, latency, complexity, and human discipline — and the question is only ever which currency your setting has to spend. ## Start from who shapes the graph The first question is not about memory at all. It is: *who decides what points at what?* 1. **Your code, entirely.** A fixed set of types whose relationships you wrote. The convention works, because someone can look at every ring and place the non-owning edge on the containment back-edge. This is the cheapest answer and the one to reach for first. 2. **Your code plus your users' data.** A document with arbitrary cross-links, a graph the user builds, a memoising cache whose entries hold the cache. Nobody authored the ring, so nobody placed the edge. Machinery is required. 3. **Code you do not control.** Extensions, plugins, embedded scripts, callbacks registered by other teams. Discipline is not available as a mechanism here, and a convention in your documentation is not a guarantee about someone else's object graph. ## The constraints that decide between the two mechanisms - **Can the roots be enumerated exactly?** If not, a reachability pass is not a slower option; it is not an option, because an approximate root set can free something still in use. This precondition is frequently the deciding factor, and it is where candidates most often hand-wave. - **What is the latency budget?** A collector working from candidates does bounded chunks of work tied to recent activity; a full pass scales with the live set, which is precisely the number that grows as the product succeeds. - **What is the memory budget?** On a constrained device, drift between passes is itself the failure, which argues for prompt candidate-driven collection over an occasional sweep. - **How exact are the counts?** Deferred or coalesced counting gives approximate values between flushes, and trial deletion over approximate counts can condemn live objects. That combination has to be designed deliberately, not assumed. - **What can you actually ship?** A cycle collector is a substantial piece of machinery with a failure mode that frees live memory. If the team cannot own it, an occasional reachability pass — where the roots permit one — is the more honest choice. ## Combining them, which is what usually happens The options are not exclusive, and the strongest answer treats them as layers: - Keep the convention as the first line for the shapes you author: it is free, and every ring it prevents is a ring the mechanism never has to find. - Run the mechanism for the shapes you do not author, and accept that it will mostly find rings created by data rather than by code. - Keep the more complete option available in test or canary builds even where you do not ship it, purely as an oracle. ## Getting evidence that the choice is working A convention is the only one of the three that can fail **silently**, so design the evidence up front: - Run the more complete mechanism in a soak or canary build and report everything it reclaims; each object it frees is a ring the convention missed, and the report names the types involved. - Treat a non-zero finding as a defect in the ownership rule rather than as the mechanism doing its job. - Watch the steady-state footprint across a long run rather than at a moment, because ring leaks accumulate at whatever rate the responsible code path runs. - Write down what would make you revisit the decision — a new extension surface, a graph that becomes user-shaped, a latency budget that tightens — because the decision is a bet on the shape of the graph, and that shape changes. ## What an interviewer is listening for The weak answer picks a mechanism immediately, usually the most sophisticated one. The strong answer asks who shapes the graph, notices that the reachability pass has a precondition rather than merely a price, states which currency this particular system can afford to spend, and finishes with the evidence that will tell them the bet was wrong.
- Why can a backup reachability pass be unavailable rather than merely expensive?Because it needs every root found exactly — stacks, registers, handles held by foreign code. Counting is often chosen precisely because that enumeration is impossible or unsafe in the setting. Where the root set can only be approximated, the pass does not just cost more; it can free an object that is still in use.
- How would you tell whether the convention is actually holding?Give it an oracle. Run the more complete mechanism in a soak or canary build and report what it reclaims: anything it frees is a ring the convention missed, and the report names the types involved. Sustained zero findings over a long run is the only real evidence that a hand-placed rule is still being followed.
- What makes this a judgment call rather than a default?Each option charges a different currency — run-time cost, a pause, machinery to own, or human discipline — and only one of them is cheap in any given setting. A latency-bound service refuses the pause, a memory-bound device refuses the drift between passes, and a host running third-party extensions cannot rely on discipline at all.
saying these in an interview costs you the question
- Picks a cycle collector before asking who shapes the object graph
- Assumes a reachability pass is always available if you pay for it
- Treats a convention as enforced because it is in a style guide
- Claims a cycle collector removes the need to think about ownership edges
- Argues the leak is harmless because each individual ring is small
- Ignores that trial deletion needs counts that are exact