Write Barriers: Garbage Collectors and Memory Consistency
Modern garbage collectors run concurrently with application threads, reclaiming memory without stopping the world for long periods. This creates a fundamental problem: while the collector traces reachable objects, application threads continue mutating the object graph. Write barriers solve this synchronization challenge by intercepting pointer writes and notifying the collector of changes that might affect reachability.
The Core Problem
A concurrent collector must maintain a consistent view of which objects are reachable. If an application thread stores a pointer to object B in object A after the collector has already scanned A, the collector might never discover B. Without intervention, B gets reclaimed even though it’s still live, causing memory corruption.
Stopping the world for the entire collection avoids this issue but introduces unacceptable pause times for latency-sensitive applications. Write barriers enable incremental and concurrent collection by tracking mutations as they happen.
How Write Barriers Work
A write barrier is a small piece of code inserted by the compiler before every pointer store operation. When application code executes object.field = value, the barrier runs first, recording information about the write before updating memory.
The barrier’s exact behavior depends on the collector’s algorithm. Snapshot-at-the-beginning (SATB) barriers, used by G1GC and Shenandoah, preserve the object graph as it existed when collection started. When a pointer is overwritten, the barrier records the old value so the collector can still trace it. This prevents an object from becoming invisible if it was reachable at the snapshot moment.
Incremental update barriers, used by the original Concurrent Mark-Sweep (CMS) collector, track new references instead. When a pointer is stored, the barrier marks the target object or adds it to a work queue. This ensures newly created paths to objects don’t go unnoticed.
Performance Implications
Write barriers add overhead to every pointer store in the application. The cost varies by implementation. A simple SATB barrier might check if collection is active, then conditionally push the old pointer value onto a thread-local buffer. This typically costs a few CPU cycles and a branch.
The overhead becomes significant in write-heavy workloads. Mutating large data structures repeatedly can spend measurable time in barrier code. JVM implementations use various tricks to minimize this: barriers are often inlined, buffer operations are batched, and some barriers can be elided through escape analysis when the compiler proves an object is thread-local.
Generational collectors exploit the observation that most objects die young. By focusing collection on the young generation, they reduce the write barrier footprint. Only pointers from old to young objects need tracking, so barriers can sometimes skip work when both objects are in the same generation.
Variants and Tradeoffs
Card marking is a coarser-grained barrier used primarily in generational collectors. Instead of recording individual pointer writes, the barrier marks a “card” representing a small region of memory (typically 512 bytes). During collection, the collector scans all marked cards to find cross-generational references. This trades precision for lower per-write overhead.
Read barriers exist but are less common. They intercept pointer reads instead of writes, used by some copying collectors to implement Brooks-style forwarding pointers. Read barriers tend to have higher overhead since reads vastly outnumber writes in most programs.
Some modern collectors like ZGC use load barriers with colored pointers, encoding metadata directly in the pointer value. This shifts work from writes to reads but enables aggressive concurrent compaction without relocating objects during pauses.
Why This Matters
Write barriers represent a fundamental tradeoff in managed runtime design. They’re invisible tax on mutation that enables low-latency concurrent collection. Understanding their behavior helps explain why some operations are unexpectedly expensive in garbage-collected languages and why certain coding patterns affect GC performance.
The evolution from stop-the-world to concurrent collectors would be impossible without write barriers. They’re the synchronization primitive that makes soft real-time garbage collection viable, enabling managed languages to serve latency-critical workloads that once required manual memory management.