skip to content

Why is it safe for a concurrent marker's write barrier to shade an object grey that turns out to be garbage?

level: middleimportance: should knowfreq 44%

answer

  1. one direction of error is fatal
  2. the hook runs on a reference store
  3. retention costs memory, not correctness
  4. kept-but-dead is reclaimed next cycle
  5. shade unconditionally, never prove liveness

basics

~20 s

Marking is allowed to err in one direction only. Keeping a dead object costs a cycle's worth of memory and is collected next time, while freeing a live one corrupts the program — so barriers over-approximate liveness on purpose.

solid answer

~40 s

A write barrier is a small hook the runtime runs on a reference store while marking is in progress. It fires on a heap *event* — a reference being installed, or an old reference being overwritten — and it has no way to know whether the object involved is still needed, so it simply shades it grey and lets the marker scan it. That is deliberate. An object retained wrongly is floating garbage: it stays marked for this cycle, the next trace starts from the roots and finds it unreachable, and the only cost is the memory it occupies in between. An object freed wrongly is a dangling reference the program will hit at some unpredictable later point. The two errors are not comparable, so every barrier design rounds towards retention.

go deeper

for a junior

Remember the asymmetry: a collector is allowed to keep something it did not have to keep, and is never allowed to free something still in use.

for a middle

Explain what event the hook fires on and why it shades without checking anything — it sits on a hot store path with no global view of the reference graph.

for a senior

Talk about what you would actually observe: a live set carrying roughly one cycle of extra garbage, versus a failure you would never debug — a reference into memory freed under a running thread.

for a principal

Treat it as a risk-weighted design choice. Over-retention is a measurable, budgeted cost; under-retention is an unbounded correctness liability, which is what justifies paying on every reference store.

## The hook and the moment it fires While a collector marks concurrently, the program keeps rewriting reference fields. A **write barrier** is the instrumentation that makes those rewrites visible to the marker: a short sequence the runtime executes alongside a reference store, for the duration of a marking cycle. Two families of write barrier exist, and they watch different halves of the same event: - an **insertion** barrier (incremental update, after Dijkstra) reacts to a reference being *installed* into an object and shades the newly stored target grey; - a **deletion** barrier (snapshot at the beginning, after Yuasa) reacts to a reference being *overwritten or dropped* and shades the old target grey before it disappears. In both cases the barrier's whole output is the same: something becomes grey, which means the marker will walk it. Neither family asks whether the object it shaded is still wanted by anyone. ## Two errors, two very different bills Marking can be wrong in exactly two ways, and they are not symmetric: | Error | What the collector does | What it costs | |---|---|---| | Over-approximation | marks an object that is already unreachable | that object's memory is held for one more cycle | | Under-approximation | leaves a reachable object unmarked | the object is freed while a live reference still points at it | The first is a budget item. The second is a memory-safety failure: the freed space is handed out again, two unrelated pieces of the program end up writing over one another, and the eventual crash or wrong answer appears far away from both the collector and the store that caused it. No amount of the first is worth a chance of the second, which is why every conservative decision inside a barrier points the same way. ## Why the barrier cannot afford to be precise It is tempting to ask why the barrier does not simply check whether the object still matters before shading it. Three things stand in the way: 1. **It has no global view.** Deciding that an object is unreachable is what the trace itself is for; asking the question inside a store is asking the collector to finish before it starts. 2. **It runs on the hottest path in the system.** The hook executes alongside ordinary reference stores, so every extra test is paid millions of times per second by the program, not by the collector. 3. **The answer would be stale anyway.** Whatever the barrier concluded, another thread could invalidate it during the same instant, which is exactly the instability the invariant exists to tolerate. So the barrier trades precision for a decision it can make locally and in a few instructions: shade, and let the trace sort out the truth. ## Where the over-approximation goes An object retained by a barrier is not special afterwards. It is scanned, blackened, and survives the cycle like any other marked object — **floating garbage**. Colours are reset for the next cycle, which traces the graph as it is then; if the object really is unreachable, that trace leaves it white and it is reclaimed. That bounds the imprecision in an important way. Conservatism of this kind costs *at most one cycle of delay per object*; it does not accumulate, and it is not a leak, because a leak is memory that stays reachable and therefore never comes back at all. A heap that grows without limit under a concurrent marker is telling you that collection cannot keep up with allocation, which is a rate problem — not a consequence of barriers rounding the wrong way. ## The one place conservatism is not allowed The barrier may over-shade freely, but it may not be *skipped*. The invariant is a property of every reference store in the system, so a single store path that does not run the hook — a bulk copy routine, a store emitted by an optimiser that lost track of the barrier, a specialised fast path added later — reopens the hazard for exactly the objects that travel that path. The failure signature is brutal: rare, far from the cause, and dependent on a timing window that a test suite almost never hits. This is why runtimes funnel reference writes through a single construct, and why removing a barrier on the grounds that a particular store "cannot matter" is a correctness change rather than an optimisation. ## The shape of the trade in one line A barrier buys a safety property with a throughput tax on reference stores and a bounded amount of retained garbage. Both of those are things you can measure and budget for. The alternative error — a reachable object left unmarked — is not something you can budget for at all, and that asymmetry, not any clever accounting, is why shading is unconditional.

  • If a barrier only ever over-approximates, can it make a program's memory use grow without bound?
    Not by itself. Objects retained by a barrier are ordinary marked objects, and the next trace starts from the roots again and finds them unreachable, so the over-approximation lasts one cycle. Unbounded growth means cycles are not completing fast enough for the allocation rate, or something genuinely reachable is being held — a different problem with a different fix.
  • What is the practical failure mode of a barrier that is correct but occasionally not executed?
    A store path that bypasses the hook reintroduces the hazard for exactly the objects that travel it. The result shows up rarely, long after the store, as a reference into memory that has been freed and handed out again. That is why reference writes are funnelled through one construct and why eliding a barrier is treated as a correctness change.

saying these in an interview costs you the question

  • Thinks the barrier decides whether an object is live before shading it.
  • Calls retained-but-dead objects a memory leak rather than a one-cycle cost.
  • Believes freeing a reachable object merely wastes work and is recoverable.
  • Assumes a barrier may be skipped on a store path that looks harmless.
  • Says the hook must run on every field write, including non-reference ones.