skip to content

Why does a write record for old-to-young references usually mark a fixed-size heap block rather than the exact field that was written?

level: middleimportance: should knowfreq 40%

answer

  1. a store on a very hot path
  2. conservative beats exact here
  3. mark a block, not a field
  4. precision paid back at collection time
  5. dirty blocks become extra roots

basics

~20 s

Cost on the hot path. A reference store happens constantly, so the record is made as cheap as a shift and a byte write: mark the block containing the field dirty. Precision is paid back later, by scanning dirty blocks for references into the young area.

solid answer

~40 s

Collecting the young area alone means the mature area is not traced, so a reference stored from a mature object into a young one would be missed. The runtime therefore records such writes as they happen. Recording the **exact** field means a growing, deduplicating data structure updated on a path that runs on almost every reference assignment — too expensive. Marking a fixed-size block (a **card**) that contains the field costs an address shift and a one-byte store, with no allocation, no growth and nothing to deduplicate. The imprecision is settled at collection time: the collector scans every dirty block, treats any reference it finds into the young area as an extra root, and cleans the block. It trades a tiny constant at write time for a scan proportional to the dirty area.

code

pseudocode · 14 lines
pseudocode
// runs on every reference-field store
write_ref(obj, field, new_value):
    obj.field = new_value
    card = (address_of(obj.field) - heap_start) >> CARD_SHIFT
    card_table[card] = DIRTY        // unconditional: no test of where new_value lives

// runs at the start of a young-area collection, world stopped
add_old_to_young_roots():
    for card from 0 to card_count - 1:
        if card_table[card] == DIRTY:
            card_table[card] = CLEAN
            for each reference r stored in block_of(card):
                if r points into young_area:
                    add_root(r)     // a dirty card with no such r is a false positive

go deeper

for a junior

Know that collecting only the young area needs help: something has to remember when an older object starts pointing at a new one, or a live object could be reclaimed by mistake.

for a middle

Explain the barrier as code on a very hot path, and say what marking a block buys and costs — a constant-time store now, a scan of whole blocks and some wasted work later.

for a senior

Reason about the scan as real production cost. Relate dirty-area growth to a write-heavy workload, and know that the record is used at every young collection rather than only at mature ones.

for a principal

Compare the families as design points: a flat dirty-block map is cheap and undirected, while per-region sets cost more to maintain but make collecting one mature region at a time possible.

## The problem the record solves A young-area collection traces from roots and reclaims the young area without tracing the mature area — that is the whole economy of the split. But an object in the young area may be reachable **only** through a reference held by a mature object. If the collector cannot see that reference, it will treat a live object as garbage and reclaim it. The mature area therefore must contribute roots. Scanning all of it would destroy the saving. So the runtime instead records the writes that could create such a reference, at the moment they happen, using a small piece of code inserted around reference stores: a **write barrier**. ## Two ways to record 1. **Exactly.** Store the address of each field that now holds a mature-to-young reference. The collector then visits precisely those fields. 2. **Conservatively.** Divide the heap into fixed-size blocks and keep one entry per block. On a reference store, mark the block containing the written field as dirty. The collector scans whole dirty blocks looking for references into the young area. The conservative scheme is the classic **card marking**: the blocks are cards, the entry array is the card table. ## Why the conservative scheme wins at write time Reference stores are among the most frequent operations a program performs. The barrier's cost is multiplied by that frequency, so its shape matters more than its precision: - marking is a **fixed, tiny sequence**: compute the field's address, shift it right by the card size, store a byte into the table; - it **allocates nothing** and the table never grows, so there is no failure path and no memory to bound; - it needs **no deduplication** — writing dirty over dirty is harmless, whereas an exact log must either grow without limit or check for duplicates; - it need **not test** where the target lives; a barrier may include such a filter, but correctness does not require it, and an unconditional store avoids a branch on the hottest path in the runtime. The table itself is small. A 4 GB heap covered by 512-byte cards at one byte per card needs about 8 MB — roughly 0.2% of the heap. ## What the coarseness costs at collection time Every saving at write time reappears as work later: - A dirty card must be **scanned in full** to find the references it contains, so the collector reads memory it may not need. - Cards go dirty for writes that never created a mature-to-young reference at all — a store of a mature-to-mature reference dirties its card just the same. - A card can remain dirty after the reference that dirtied it has been overwritten. Such a **false positive** is scanned, yields nothing, and is cleaned. - Scanning cost scales with the **dirty area**, so a write-heavy program with references scattered across the mature area can spend real time here. None of this threatens correctness: marking a block is conservative in the safe direction, covering every field in it. The record can report references that are not there; what it must never do is omit one that is. ## Choosing the block size | card size | table memory | scan per dirty card | typical effect | |---|---|---|---| | smaller | larger | smaller | finer precision, more table to touch and to keep in cache | | larger | smaller | larger | cheaper table, more wasted scanning per write | The choice is an empirical balance, and it interacts with how references are distributed: a program that writes references in tight clusters wastes little with large cards, while one that scatters single writes across a big mature area pays for every one of them. ## A more precise alternative A second family keeps, per region of the heap, a **remembered set** of the locations that hold references into that region. This is more precise and, crucially, lets a collector reclaim one region of the mature area without scanning the rest — which a single flat dirty-block table cannot support. The price is a heavier barrier: the write must be filtered, the entry recorded into a real data structure, and duplicates suppressed. Such sets can also become large when cross-region references are dense, and designs that use them need a policy for what to do when one grows beyond its budget. ## What an interviewer is listening for That you explain **why** the record exists before explaining its shape; that you identify the write barrier's frequency as the thing being optimised; and that you can name the imprecision it buys — scanning whole blocks, and false positives that survive an overwrite.

  • What happens to a block marked dirty whose reference is later overwritten with a target in the mature area?
    It stays dirty until the next collection scans it. The scan finds no reference into the young area, produces no roots and cleans the block — a false positive that cost a scan and nothing else. Clearing the mark eagerly on overwrite would need a test and a second store on the same hot path, which is exactly what the scheme is avoiding.
  • How does block size trade off against the cost of the scheme?
    Smaller blocks mean a bigger table and finer precision, so each dirty block is quick to scan but the table itself costs memory and cache. Larger blocks shrink the table and make marking no cheaper, while every dirty block costs more to scan. The right size depends on how tightly a workload clusters its reference writes.

saying these in an interview costs you the question

  • Thinks marking a whole block can miss a reference into the young area
  • Assumes a barrier must test the target's area to be correct
  • Says a dirty block still contains a live old-to-young reference at collection time
  • Believes the record is consulted only when the mature area is collected
  • Thinks the collector traces through the mature objects found in dirty blocks