The idea: a minor GC must not read the old generation

A minor GC collects only the young generation (eden and the survivor spaces). It finds live young objects by following references from the roots: thread stacks, static fields, JNI handles. But an old object can also point to a young one. If the GC ignored that reference, it would free a live object. If it traced the whole old generation to find such references, a minor GC would be as slow as a full GC.

A row of 8 card table bytes above 8 old-generation cards holding objects A to H. Byte 130 is 00 (dirty), the others FF (clean). Object C on card 130 points to young object Y3 in eden; the thread stack points to Y1.
The card table remembers which 512-byte cards were written; the minor GC reads only those.

HotSpot's answer is the card table: a cheap, rough record of where old→young references might be. The GC treats every object on a dirty card as an extra root and skips the rest of the old generation.

Cards and the card table

The heap is cut into cards of 512 bytes (GCCardSizeInBytes, default 512). The card table is a byte array with one entry per card, so the card of address a is card_table[a >> 9]. In HotSpot a clean card is 0xFF and a dirty card is 0x00. On the page the old generation is 0x10000–0x10FFF, so its cards are 128 to 135.

Old generationCardsCard table size
4 KB (this page)88 bytes
1 GB2,097,1522 MB
32 GB67,108,86464 MB

Walking 2 MB of card bytes is far cheaper than following every reference in 1 GB of objects, and a clean card costs one byte read.

The post-write barrier

The JIT and the interpreter add a few instructions after every reference store into a heap object, the post-write barrier:

C.f = y;                              // putfield
card_table[address_of(C.f) >> 9] = 0;   // mark the card dirty

In machine code it is a shift and a byte store (the table base is pre-adjusted so no subtraction is needed). The Serial and Parallel barrier does not test the value: storing null, an old object or a young object all dirty the card, because a test would cost more than the byte store. (Demo 1: old→young store dirties one card; Demo 3: old→old store, card scanned for nothing.) With -XX:+UseCondCardMark the barrier first checks whether the card is already dirty, which avoids cache-line ping-pong between cores writing nearby cards.

Minor GC with dirty cards as roots

Five steps top to bottom: C.f = new Object(); the barrier writes 0 to card_table[130]; the minor GC walks the card table and scans card 130, finding Y3 live; Y3 is copied to survivor and C.f fixed; card 130 stays dirty because C.f still points into the young generation.
A store dirties a card; the next minor GC scans it and keeps it dirty only while it still points into the young generation.
  1. Stop the world and mark from the stack roots.
  2. Walk the card table. Skip clean cards. For each dirty card, scan the objects on it; every reference into the young generation is a root.
  3. Copy the live young objects to the empty survivor space and fix the references to them. Eden is then empty; whatever was not reached is gone.
  4. Clean the scanned cards, except those whose objects still point into the young generation (now the survivor space): they stay dirty for the next minor GC.

A dirty card does not mean an old→young reference exists, only that something on it was written. Setting the field to null leaves a dirty card that the GC scans for nothing, then cleans, and the young object it used to reference dies (Demo 2: field set to null, young object dies). When eden is full, new Object() fails and the same GC runs first (Demo 4: eden full, allocation failure GC).

What goes wrong without the barrier

If a store skipped the barrier, its card would stay clean. The minor GC would not scan it, would not see that C points to Y3, and would free Y3 with the rest of eden. C.f would then hold the address of free memory, which the next allocation reuses: a dangling pointer and silent heap corruption (Demo 5: missing barrier frees a live object). That is why every collector that collects part of the heap needs a barrier, and why sun.misc.Unsafe stores go through it too.

G1: remembered sets built from cards

Left: Parallel GC dirties a card on every store and the minor GC walks the whole card table. Right: G1's barrier skips same-region, null and young stores; a dirty card goes to a queue, a refinement thread adds it to young region R2's remembered set, and the young GC scans only those remembered sets.
Parallel GC scans one global card table; G1 keeps a remembered set per region, filled by refinement threads.

G1 splits the heap into regions (1–32 MB) and still uses 512-byte cards, but adds a remembered set per region: the set of cards in other regions that may point into it. Its post-write barrier filters more: a store within the same region, a null store, or a store into a young region does nothing. Otherwise the card is dirtied and queued; concurrent refinement threads scan queued cards and add them to the remembered sets of the regions they point into. A young GC then scans only the remembered sets of the young regions (and the cards still queued), and a mixed GC uses the remembered sets of the old regions it evacuates. The barrier is more expensive, but GC pauses no longer scale with the size of the old generation. See Java Garbage Collectors: Parallel GC, G1, ZGC for regions and mixed GCs.

Serial / ParallelG1
barrieralways dirty the cardfilter, dirty, enqueue
where old→young refs are keptone global card tablea remembered set per region
young GC readsthe whole card table + dirty cardsyoung regions' remembered sets

Go uses a different kind of write barrier for a different job: keeping concurrent marking correct (Go's Garbage Collector). How objects get to the heap in the first place is on Stack vs Heap in Java.

What the page leaves out

Tenuring by age (MaxTenuringThreshold; here an object is promoted only when the survivor space is full); objects larger than a card and object arrays, which are scanned in card-sized chunks; the precise object-start lookup (block offset table) needed to find the first object on a card; young-to-young and stack-to-young being found by copying (Cheney scan); parallel card scanning by GC worker threads; G1's pre-write (SATB) barrier for concurrent marking; and ZGC and Shenandoah, whose generational modes use their own barriers and remembered-set bitmaps.