Concurrent and Parallel Sweeping in V8 Orinoco
This article explains how the Orinoco garbage collector in Google’s V8 engine executes concurrent and parallel sweeping to reclaim memory in JavaScript. By shifting the workload from the main JavaScript execution thread to background worker threads, Orinoco drastically reduces stop-the-world pause times. The following sections break down the sweeping mechanics, page-level isolation, and how parallel and concurrent sweeping work in tandem to maintain smooth application performance.
The Role of Sweeping in V8
In V8, garbage collection operates in major phases: marking, sweeping, and compacting/evacuating. During the marking phase, the collector traverses the object graph to identify live objects and sets corresponding mark-bits.
Sweeping is the subsequent phase responsible for reclaiming memory occupied by dead objects (unmarked memory). Instead of freeing memory byte-by-byte, the sweeper processes memory pages, identifies contiguous spans of dead memory, and rebuilds “free-lists.” These free-lists are later used by the memory allocator to fulfill new JavaScript object allocations.
Parallel Sweeping
Parallel sweeping distributes the sweeping workload across multiple worker threads simultaneously, typically during periods where the main thread is paused or actively coordinating garbage collection tasks.
- Task Scheduling: V8 divides the heap into discrete, fixed-size memory pages (commonly 256 KB to 1 MB depending on the heap configuration).
- Work Stealing: The engine initializes a pool of background sweeper tasks. Worker threads pull un-swept pages from a shared work queue using atomic operations.
- Execution: Multiple cores process different pages concurrently, iterating through mark-bit bitmaps, assembling free-lists for each page, and updating page metadata without thread contention.
Because each worker thread processes distinct pages, parallel sweeping scales efficiently with the number of available CPU cores.
Concurrent Sweeping
While parallel sweeping uses multiple threads, concurrent sweeping runs these background threads simultaneously alongside the main JavaScript execution thread, preventing the browser or Node.js runtime from freezing.
- Main Thread Resumption: Immediately after the marking phase concludes, the main thread resumes executing JavaScript code.
- Background Sweeping: Worker threads continue sweeping pages in the background, populating free-lists without blocking user interactions, animations, or I/O operations.
- Page-Level Granularity: To avoid data races, the heap allocator does not touch a page while it is actively being swept by a background worker.
Preventing Race Conditions and Allocation Latency
Running sweeping concurrently with active JavaScript execution introduces potential synchronization issues, which Orinoco resolves through structured allocation patterns:
- Page States and Locking: Pages are tagged with
state indicators (e.g.,
sweeping-in-progress,swept). The main thread’s allocator skips pages marked as actively sweeping when searching for free space. - On-Demand (Lazy) Sweeping: If the JavaScript main thread requires a new memory allocation and runs out of pre-swept free-lists, it can selectively sweep a single page itself on demand rather than waiting for background workers to complete the entire heap.
- Linear Allocation Buffers (LAB): The allocator hands out contiguous chunks of swept memory to the main thread in local buffers, minimizing atomic synchronization checks during high-frequency object allocations.
By decoupling the sweeping phase from the main JavaScript execution loop and dividing page processing across background workers, Orinoco transforms what was historically an expensive stop-the-world pause into a lightweight, non-blocking background operation.