Write barriers
A write barrier is a tiny piece of code that runs on (almost) every pointer store in JavaScript. It is the price the mutator pays so that the Scavenger can collect the young generation without scanning all of old space, and so that concurrent marking stays correct while JS keeps mutating. Understanding it explains a cost that shows up everywhere in V8-generated code.
::: info Ubiquitous language Mutator: the running JavaScript (it mutates the heap). Write barrier: code on a pointer store that informs the GC. Remembered set: the recorded set of "interesting" pointers (e.g. old→young). Tri-color invariant: a black object must never point to a white one (or the white one would be wrongly collected). :::
Two problems, one barrier
Generational problem. The Scavenger collects young space by
scanning roots and references into young space. But old objects can point to
young objects (oldObj.next = youngObj). Scanning all of old space to find such
pointers would defeat the point of a cheap minor GC. So V8 records every
old→young pointer as it is created, in a remembered set, and the Scavenger
scans only those.
Concurrent-marking problem. While concurrent markers traverse the graph, the mutator might store a pointer from an already-scanned (black) object to an unmarked (white) one. If unrecorded, that white object could be wrongly collected. The barrier catches such stores and keeps the tri-color invariant.
V8 handles both in one combined barrier on the store path:
void WriteBarrier::CombinedWriteBarrierInternal(Tagged<HeapObject> host,
HeapObjectSlot slot, Tagged<HeapObject> value, WriteBarrierMode mode) {
MemoryChunk* host_chunk = MemoryChunk::FromHeapObject(host);
// Fast path: marking off AND host is young/shared → no bookkeeping needed.
if (V8_LIKELY(!host_chunk->PointersFromHereAreInteresting())) return;
MemoryChunk* value_chunk = MemoryChunk::FromHeapObject(value);
if (!value_chunk->PointersToHereAreInteresting()) return; // old→old, marking off
CombinedWriteBarrierInternalSlow(host, host_chunk, slot, value, value_chunk);
}
— src/heap/heap-write-barrier-inl.h#L27-L79
The fast path is (almost) free
Look at the structure: the common case bails out in a couple of instructions. A
store whose host is in young space needs no remembered-set entry (the
Scavenger scans young space anyway), and when marking is off, an old→old store
needs nothing either. Per-page flag bits (PointersFromHereAreInteresting,
PointersToHereAreInteresting) make these checks branch-cheap. The expensive
"slow" path — actually inserting into a remembered set or marking the value — only
runs for the genuinely interesting minority of stores.
This is the design ethos: a barrier on every store must be nearly free in the common case, or it would tax all of JavaScript. V8 spends a few bits of per-page metadata to buy a one- or two-instruction fast path.
The remembered set
When a store is interesting (old host, young value), the slot is recorded:
template <RememberedSetType type>
class RememberedSet : public AllStatic {
template <AccessMode access_mode>
static void Insert(MutablePage* page, size_t slot_offset) {
SlotSet* slot_set = page->slot_set<type, access_mode>();
if (slot_set == nullptr) slot_set = page->AllocateSlotSet(type);
RememberedSetOperations::Insert<access_mode>(slot_set, slot_offset);
}
};
— src/heap/remembered-set.h#L25-L100
Remembered sets are per page (OLD_TO_NEW, OLD_TO_OLD, etc.), storing slot
offsets in a compact SlotSet, allocated lazily only for pages that actually
contain interesting pointers. At scavenge time, the Scavenger iterates the
OLD_TO_NEW set instead of all of old space — converting an O(old-space-size)
scan into O(interesting-slots).
::: details Where the barrier comes from in generated code
The interpreter, Sparkplug, and the optimizing compilers
all emit the write barrier inline at pointer stores (often the
PointersFromHereAreInteresting check, falling through to a builtin for the slow
path). The optimizers can sometimes elide it — e.g. storing into a freshly
allocated young object that is provably still young needs no barrier. Barrier
elimination is a real optimization the compilers perform.
:::
Why JavaScript pays this tax
The write barrier is the quiet enabler behind two of Orinoco's biggest wins:
Without it, the cheap generational Scavenge would be impossible (you'd have to scan all of old space every minor GC).
Without it, concurrent marking would be unsound (the mutator could hide live objects from the markers).
So every a.b = c in your program does a hair more work than a raw store — and in
exchange, GC pauses are short and minor GCs are cheap. It is one of the clearest
cost/benefit trades in the engine.
See also
Scavenger — the consumer of the old→young remembered set.
Concurrent & incremental marking — needs the marking barrier.
C++ primer: handles — the other half of GC-safe code.