V8pedia

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