skip to content

A young-generation collection must not miss an object whose only reference comes from the old generation, yet it does not scan the old generation. What runtime structure makes that possible, what does the JVM do on every reference-field write to maintain it, and how does a region-based collector's version differ from a simple card table?

level: middleimportance: must knowfreq 52%

answer

  1. old→young refs must be roots for a minor GC
  2. card table: 512-byte cards, 1 byte each, dirty marker
  3. post-write barrier = shift + store, unconditional
  4. dirty card ⇒ scan the card, not the old gen
  5. region collectors: per-region remembered set + dirty card queue + refinement threads

basics

~30 s

Cross-generational references are recorded. A write barrier runs on every reference store and marks the containing card (a small fixed-size chunk of the heap, 512 bytes in HotSpot) dirty in a card table. A young collection then treats objects in dirty cards as extra roots, scanning only those cards instead of the whole old generation. Region collectors keep per-region remembered sets of incoming references instead of one global table.

solid answer

~1 min

Correctness requires that any old-to-young reference be treated as a root for a young collection. Scanning the old generation to find them would destroy the point of a generational collector, so the JVM records them as they are created. HotSpot's classic mechanism is a **card table**: the heap is divided into cards (512 bytes) and there is a one-byte entry per card. The JIT and interpreter emit a **post-write barrier** after every reference store — compute the card index from the object address by shifting, store a dirty marker into the table. It is unconditional and a couple of instructions, so it is cheap and does not need to know whether the reference actually crosses generations. At collection time the collector scans dirty cards, finds the references they contain, and adds any that point into the young generation to the root set. The trade is precision for speed: a dirty card is scanned even if the reference has since been overwritten. Region-based collectors need more: they collect an arbitrary *subset* of regions, so each region carries a **remembered set** of the locations elsewhere that point into it. Their barrier filters same-region and null stores and enqueues cards for concurrent refinement threads that update the per-region sets off the pause.

code

text · 6 lines
text
// source:  a.field = b;
// emitted: the store, then the card mark
mov   [rax + field_offset], rbx        ; the reference store
lea   rcx, [rax + field_offset]
shr   rcx, 9                           ; 9 == log2(512) card size
mov   byte [card_table_base + rcx], 0  ; 0 == dirty in HotSpot's encoding

go deeper

for a junior

Know that old-to-young references are tracked so the minor collection can use them as roots without scanning the old generation, and that the tracking happens on reference writes.

for a middle

Describe the card table concretely — fixed-size cards, a byte per card, an unconditional post-write barrier of a shift plus a store — and explain the imprecision it accepts.

for a senior

Explain why region-based collectors need per-region remembered sets, how the filtering barrier plus dirty-card queues plus refinement threads move the cost off the pause, and what it looks like when refinement falls behind.

for a principal

Treat the barrier as a permanent throughput tax on mutator reference writes and remembered sets as a heap-memory tax, and use that to judge when a mutable-heavy design or an alternative collector is the right call.

## The correctness problem A generational collector's central claim is that a young collection can complete without examining the old generation. But reachability does not respect generation boundaries: a long-lived cache in the old generation may hold the only reference to a young object. If the collector traced only from thread stacks and statics, it would declare that object dead and reclaim memory that is still in use — a catastrophic bug. The formal requirement is that the root set for a young collection must include *every* reference from outside the collected region into it. The weak generational hypothesis says such references are **rare**, not that they are absent. So the collector's job is to record them cheaply as they appear, rather than to hunt for them at collection time. ## Card tables HotSpot's classic answer is the card table. Conceptually the heap is partitioned into fixed-size **cards** — 512 bytes in HotSpot — and a byte array holds one entry per card. Marking a card *dirty* means: "some reference field inside this 512-byte chunk was written since the last collection; look here." The table is maintained by a **write barrier**: a short instruction sequence the interpreter and the JIT emit after every reference-field store (`putfield`/`aastore` on reference types, not on primitives). The classic form is about as simple as code gets: ``` cardTable[(address_of_field) >> 9] = DIRTY; ``` A shift and a byte store. There is no branch on whether the reference actually crosses generations — testing that would cost more than the occasional false positive. This is called an *unconditional* card-marking barrier, and its cost is one of the standing overheads of a generational JVM, paid on every reference assignment in the program. At collection time the collector walks the card table, and for every dirty card parses the objects in that chunk of heap and examines their reference fields. References pointing into the young generation become roots; the rest are ignored. The card is then cleaned. This is deliberately **imprecise** in two directions. A dirty card names a 512-byte area, not a field, so up to a card's worth of objects is scanned to find one reference. And the card stays dirty even if the reference was later nulled out. Imprecision costs a little scanning time and buys a barrier of two instructions — historically a very good trade. There is a subtlety on multiprocessor machines: unconditional marking means many threads write the same card bytes, causing false sharing on the cache lines holding the table. HotSpot has a *conditional* card-marking variant that reads the byte first and skips the store if it is already dirty, trading a load and a branch for reduced cache-line contention. ## Remembered sets in region-based collectors A card table answers one fixed question: "which parts of the old generation might point into the young generation?" That suffices when the collected set is always exactly the young generation. A region-based collector chooses an arbitrary **collection set** of regions each cycle — some young, some old, selected by expected payoff. For that it must be able to ask, for *any* region, "who points into me?" So each region owns a **remembered set**: a structure recording the locations (typically card indices, held in per-region hash tables at varying granularities) that contain references *into* that region. When a region is evacuated, its remembered set gives the extra roots directly. Maintaining these is far more work than dirtying a card, so the barrier is split and much of the work moves off the mutator's critical path: - A **filtering write barrier** discards the common uninteresting cases inline — the reference and the field are in the same region, or the stored value is null. - Surviving stores mark a card and, if that card was not already queued, place it on a per-thread **dirty card queue**. - Background **concurrent refinement threads** drain those queues and update the affected regions' remembered sets while the application runs, so the pause does not pay for it. - If refinement threads fall behind, the mutator threads are made to help, which shows up as increased application-thread time and, in the logs, growing remembered-set update and scan costs. The economics matter: remembered sets consume real memory (a meaningful fraction of heap in pathological cases) and real CPU. A workload that constantly rewrites references from old-generation objects to young ones — a large mutable old-generation graph churning fields — makes remembered-set maintenance a dominant cost. This is one concrete way the "few old-to-young references" half of the generational hypothesis can fail even when "most objects die young" holds. ## What is *not* recorded Young-to-old and young-to-young references need no recording: the young generation is scanned in full during every young collection, so those references are found by ordinary tracing. Only references from *outside* the collected set inward must be remembered. Primitive-field writes need no barrier either, which is why array-of-`int` churn is barrier-free while array-of-reference churn is not. Finally, note that this write barrier is a *GC* write barrier — a bookkeeping hook for the collector. It is unrelated to the memory-ordering fences the JVM emits for visibility guarantees, which happen to share the word "barrier".

  • Why does the write barrier not simply check whether the reference actually crosses from the old generation into the young generation?
    Because the check costs more than it saves. The unconditional form is a shift and a byte store with no branch; adding generation comparisons means loading boundaries and taking an unpredictable branch on a code path that runs on every reference assignment in the program. The collector absorbs the resulting false positives by scanning a few extra cards, which is far cheaper in aggregate.
  • Why do young-to-old references not need to be recorded anywhere?
    Because the entire young generation is traced during every young collection, so a reference from a young object to an old object is discovered by ordinary tracing and needs no side record. Recording is only required for references entering the collected set from outside it, since that outside region is deliberately not traced.
  • What symptom in the logs suggests remembered-set maintenance has become expensive in a region-based collector?
    Growing time attributed to updating and scanning remembered sets within the young pause, and evidence that concurrent refinement threads are falling behind so mutator threads must help. It typically points at a large, mutable old-generation object graph whose reference fields are rewritten frequently, which is the second half of the generational hypothesis failing.

A card table is a change-log of neighbourhoods, not of houses: instead of noting which mailbox got a new address, you note that something changed on this street, then walk that one street later. Cheap to write, slightly wasteful to read.

saying these in an interview costs you the question

  • Claiming a young collection scans the old generation for references — that would defeat the whole design.
  • Saying the card table stores exact field addresses rather than a coarse per-card dirty bit.
  • Confusing the GC write barrier with a memory-ordering fence used for visibility guarantees.
  • Believing write barriers fire on primitive-field stores as well as reference stores.
  • Assuming a card table alone is enough for a collector that evacuates an arbitrary subset of regions.

context