Some managed runtimes decide object liveness by reference counting. Why does the JVM use tracing from GC roots instead?
answer
- cycles never reach zero
- every store = atomic inc/dec
- cascading frees inside app threads
- tracing cost scales with live set
- moving needs reference locations, not counts
basics
~20 sReference counting cannot reclaim cycles: mutually referencing objects keep each other's counts above zero forever. It also charges every reference write with a count update, which must be atomic under concurrency. Tracing collects cycles naturally and costs proportional to live data.
solid answer
~60 sReference counting stores a count per object, adjusts it on every reference assignment, and frees at zero. Its appeal is promptness and incremental cost. Its fatal defect for a general-purpose runtime is cycles: two objects pointing at each other never reach zero even when nothing else refers to them, so runtimes relying on it need a backup tracing collector or programmer-applied weak links. Java object graphs are full of cycles — parent/child nodes, listeners, doubly linked structures — so a scheme that leaks them is unusable. The costs go beyond correctness. Every reference store becomes a read-modify-write of two counters, which in a multithreaded runtime must be atomic, creating contention and cache-line traffic on popular objects. Counts consume header space. Dropping the last reference to a large graph triggers a cascade of decrements at an arbitrary point inside application code, producing unpredictable pauses. Tracing pays nothing on ordinary reference writes, handles cycles for free, does work proportional to the live set, and — decisively — yields the *locations* of references, which is what makes moving and compacting collectors possible.
go deeper
Lead with cycles: counts never hit zero for mutually referencing objects, so Java traces from roots instead and collects such islands naturally.
Add the mutator cost of updating counts on every reference store and the need for atomicity across threads.
Discuss the pause profile and the enabling of moving collection: tracing yields reference locations, which is what permits copying, evacuation, and compaction.
Present it as a design tradeoff space, noting that hybrid designs exist and that the deciding factors for the JVM were cyclic object shapes, high-mutation concurrency, and the value of relocation.
## What reference counting is Each object carries a count of how many references point at it. Every assignment that stores a reference increments the new target's count and decrements the old target's; when a count reaches zero the object is freed immediately and its outgoing references are decremented in turn. Nothing scans the heap, and reclamation is prompt. ## Defect one: cycles Consider a parent node holding a child and the child holding a parent pointer. Drop the only outside reference to the parent and both counts remain at one, because each still points at the other. Neither can ever be freed. Java programs are saturated with such shapes: bidirectional data structures, observer registrations, closures capturing their owner, graph and tree models with backlinks. A liveness rule that leaks all of them is unacceptable, which is why production reference-counting runtimes supplement it with either a tracing cycle collector or a language convention of weak backlinks the programmer must apply correctly. Tracing has no such problem, for a structural rather than clever reason: it asks whether a path from a root exists, and an isolated cycle has none, however densely its members reference each other. ## Defect two: mutator cost and concurrency Tracing charges the application nothing for a plain reference store. (Generational and concurrent collectors add a write barrier, but that is a small bounded cost tied to the collector's own bookkeeping.) Reference counting charges every store: load the old value, decrement it, increment the new one, test for zero. Across multiple threads these updates must be atomic or the counts corrupt, meaning locked instructions on the fast path, contention on popular objects, and cache-line ping-pong when several cores touch one header. That is the opposite of what a runtime optimised for many threads mutating shared structures wants. ## Defect three: pause behaviour and throughput Promptness is often sold as the benefit, but dropping the last reference to a large structure runs a cascade of decrements and frees synchronously inside the application thread at an unpredictable moment. Tracing collectors do their work in scheduled cycles that can run concurrently and can be tuned; the cost is proportional to the live set, so short-lived garbage costs almost nothing to reclaim — exactly the profile of typical Java allocation. ## Defect four: it does not enable moving Much of HotSpot's performance comes from moving objects: bump-pointer allocation into contiguous space, copying survivors, evacuating regions, compacting away fragmentation. Moving requires knowing every reference to an object so all of them can be updated, which tracing discovers as a by-product. Reference counting knows how many references exist, not where they are. ## What Java offers instead of counting When a program needs a liveness relationship that does not retain, it uses the reference API rather than counts; when it needs deterministic release of a non-memory resource, it uses explicit lifecycles such as try-with-resources. Reachability handles memory; explicit code handles what the collector does not own. ## Fair statement of the tradeoff Reference counting is not a bad idea in general — it is excellent where object graphs are acyclic and predictability matters more than throughput, and modern designs blend it with tracing. The honest interview answer is that its cycle problem is disqualifying for Java's object shapes, its per-store atomic cost is disqualifying for Java's concurrency model, and its inability to locate references would forfeit compaction and generational copying.
- Do tracing collectors really impose no cost on reference writes?Nearly none in the abstract, but real generational and concurrent collectors insert a write barrier on reference stores to record cross-generational or concurrently mutated references. That barrier is a few instructions with no atomic contention on the common path, far cheaper than an atomic counter update on every store.
- Given tracing, why does Java still have try-with-resources and explicit close?Reachability governs only memory, and reclamation timing is unspecified. File descriptors, sockets, and native handles are scarce OS resources that must be released deterministically, so the language provides explicit lifecycle constructs rather than relying on when the collector happens to notice an object is unreachable.
Counting how many people hold a rope tells you nothing about whether anyone is standing on solid ground: two people can hold each other's ropes forever while both float free.
saying these in an interview costs you the question
- Saying the JVM uses reference counting internally to decide liveness
- Claiming reference counting is merely slower, without naming the cycle problem
- Asserting that cycles leak in Java
- Ignoring that atomic count updates on every reference store are the real multithreaded cost
- Missing that tracing is what makes moving and compaction possible