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 inTrace(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.