# 2. Colored pointers, load barriers, store barriers, remembered sets > Runnable companions: [`jmh/.../LoadBarrierBench.java`](../jmh/src/main/java/com/ankurm/zgc/jmh/LoadBarrierBench.java), > [`jmh/.../StoreBarrierBench.java`](../jmh/src/main/java/com/ankurm/zgc/jmh/StoreBarrierBench.java) This is the chapter that explains *why* the numbers look the way they do. Every throughput difference between ZGC and G1 traces back to one of the mechanisms below. ## 2.1 Colored pointers A 64-bit JVM does not need 64 bits to address the heap. ZGC uses the spare high bits of every heap reference as metadata — the "color". The colors encode facts like *has this pointer been marked in the current young cycle?* and *has the object it points at been relocated?* The consequence is that **the pointer, not a side table, carries the collector's state**. Two objects can hold references to the same object with different colors, and each will be fixed up independently the first time it is read. This is the trick that lets ZGC relocate objects concurrently. Traditional compacting collectors must stop the world to fix up every reference to a moved object. ZGC does not: it moves the object, records a forwarding entry, and lets each stale reference repair itself on next read. > **Multi-mapping vs. multi-mapping-free.** Older ZGC mapped the same physical heap at several > virtual addresses so that colored pointers could be dereferenced directly. Modern ZGC (from JDK > 22's "colored roots"/load-barrier rework onward) masks the color in the barrier instead. Practical > effect: on Linux, `RSS` no longer looks like 3x the heap in tooling that misreads multi-mapped > regions. If you have old runbooks explaining that "ZGC reports 3x memory, ignore it", they are > obsolete. ## 2.2 The load barrier Every time your code reads a **reference field**, the JIT emits a short sequence: test the color bits, and on the slow path fix the pointer. Primitive loads (`int`, `long`, elements of `int[]`) are untouched — a common misconception is that ZGC taxes all memory access. It does not. It taxes reference loads. `LoadBarrierBench` prices this by comparing two walks over the same number of elements: | Benchmark | What it does | Barrier? | |---|---|---| | `chaseReferences` | walks a shuffled 1M-node linked structure, one reference load per step | yes, one per step | | `sumPrimitives` | sums an `int[]` of the same length | none | | `readSameReferenceRepeatedly` | reads one already-good reference 1024 times | yes, but always fast path | The third one is the interesting control: it measures the **floor** of the barrier — what you pay when there is nothing to fix. If `readSameReferenceRepeatedly` is close between ZGC and G1, the fast path is nearly free and any gap in `chaseReferences` is the slow path plus cache effects. Two things move these numbers a lot, and both are worth measuring on your own workload: - **Whether a GC cycle is in progress.** The barrier does strictly more work during marking and relocation. Force cycles with `-XX:ZCollectionInterval=1` to see the sustained-load case rather than the idle-heap case. - **Whether the walk is cache-resident.** A sequential walk over a small structure is dominated by the prefetcher and hides the barrier entirely. That is why the benchmark shuffles the node order — a "random" pointer chase that is secretly sequential measures the memory subsystem, not the GC. ## 2.3 The store barrier and remembered sets Generational collection requires knowing about old-to-young references (see [01-generational-model.md](01-generational-model.md) §1.3). The two collectors solve it differently and the difference is measurable. **G1: card table + concurrent refinement.** The heap is divided into 512-byte *cards*. A reference store dirties the card containing the modified field. Concurrent refinement threads later scan dirty cards and update per-region *remembered sets* (which regions point into me?). G1's barrier is a two-part affair — a pre-write barrier for SATB marking and a post-write barrier for card marking — and has historically been the more expensive of the two collectors' barriers. JDK 24 landed *late barrier expansion* for G1 (JDK-8342382), which defers expanding the barrier into IR until late in C2 so the optimiser can see through more of the surrounding code; JDK 25 continued reducing remembered-set memory. **ZGC: store barrier + per-page remembered sets.** Generational ZGC's store barrier appends to a thread-local buffer (`ZBufferStoreBarriers=true`), which is drained into per-page remembered-set bitmaps. The bitmaps are **double-buffered**: one is being written by the application while the other is consumed by the collector, which is how ZGC avoids a stop-the-world handoff. `StoreBarrierBench` separates "the barrier code ran" from "the barrier had work to do": | Benchmark | Store | Creates old→young edge? | |---|---|---| | `storeOldToYoung` | `oldArray[i] = new Object()` | yes | | `storeNullIntoOld` | `oldArray[i] = null` | no — barrier runs, records nothing | | `storeOldToOld` | `oldArray[i] = oldArray` | no | | `storePrimitiveIntoOld` | `oldIntArray[i] = i` | not a reference store; no barrier at all | The benchmark walks slots with a stride of 4099 rather than sequentially. Sequential stores hit the same card / same page repeatedly, and both collectors have an "already dirty" fast path that would hide most of the cost. This is the single most common way a store-barrier microbenchmark accidentally measures nothing. ## 2.4 Why this shows up as a throughput gap, not a latency gap Barriers cost throughput: a few extra instructions on a very hot path. They do **not** cost latency, because they are spread evenly across execution rather than concentrated into a pause. That is the whole trade in one sentence: **ZGC converts a rare, large, correlated cost (a stop-the-world pause) into a constant, small, uncorrelated cost (barrier work on every reference access).** Whether that is a good deal depends entirely on whether your SLO is expressed as a mean or as a tail percentile. --- Next: [03-allocation-stalls.md](03-allocation-stalls.md) — the ZGC failure mode that pause charts cannot see.