Blink is C++, and the DOM object graph it builds is tangled with JavaScript objects, so Chromium manages that memory with a real garbage collector: Oilpan, connected to V8 through cross-component tracing that treats the C++/JS object graph as one heap.

This V8 blog post explains the core principles, then digs into the part that shows up as page jank — reclaiming dead objects concurrently.

Marking is ordinary graph traversal: objects are nodes, pointers are edges, roots are registers, stack, and globals. C++ objects cannot change representation at runtime, so Oilpan asks them to declare their outgoing edges precisely:

  • Inherit from GarbageCollected<T> and describe pointers in Trace(Visitor*)
  • Member<T> smart pointers keep the graph consistent during traversal
  • The collector therefore knows exactly where every pointer lives — no guessing on the heap

The stack is a different story. Raw stack pointers stay raw, and Oilpan finds roots by conservative stack scanning: walk the stack word-by-word and treat anything that looks like a heap pointer as one. No steady-state cost for stack access; the cost moves to GC time, and the renderer deliberately waits for moments when the stack holds nothing interesting — easy in an event-loop-driven browser.

Sweeping in C++ has a constraint managed languages do not have: every dead object’s destructor must run before its memory is freed, and sweep iteration order does not match construction order. So destructors become finalizers with a hard rule — they may not touch other on-heap objects. Oilpan enforces that with a Clang plugin, turning a semantic rule into a compile error.

The latency story is a three-step climb:

  • Stop-the-world sweeping ran inside the finalization pause
  • Incremental sweeping split the work into idle-time main-thread tasks over pages, where allocation only draws from already-swept pages and will inline sweeping itself before asking the OS for memory
  • Concurrent sweeping hands reclamation to background threads

Concurrency here needs no locks in the hot path, just two invariants:

  • The sweeper only touches dead memory — by definition unreachable by the application
  • The application only allocates on already-swept pages — by definition no longer being swept

The tension is finalizers. Because C++ destructors can race with app code, Oilpan insists they run on the main thread, so the background sweeper pushes objects that have finalizers onto a queue for a separate main-thread phase. Memory goes to the free list immediately only when there is no finalizer; otherwise it waits for the destructor.

Measured result: background sweeping shipped in Chrome M78 and cut main-thread sweeping time by 25–50%, 42% on average, according to V8’s real-world benchmarking framework. What remains on the main thread is finalizer execution — which is why the next round of work is deleting finalizers from heavily-instantiated Blink types. Sweeping adapts on its own when they are gone.

The transferable lesson is the shape of the argument: relocate cost to the rare phase instead of taxing the common one, state concurrency safety as two properties rather than a lock, and enforce language-level rules with a compiler plugin instead of a code review.