How Major GC Mark-Compact Works in JavaScript
This article provides an overview of how JavaScript engines, such as V8, manage long-lived objects in memory using the Major Garbage Collection (GC) Mark-Compact algorithm. You will learn how objects transition to the Old Space, how the engine identifies live versus dead memory through marking, and how compaction resolves memory fragmentation to keep applications running efficiently.
The Role of the Old Space in JavaScript
JavaScript engines divide the memory heap into different spaces using a generational hypothesis, which assumes most objects die young. Short-lived objects are allocated in the New Space and managed by the Minor GC (Scavenger). Objects that survive multiple Minor GC cycles are promoted to the Old Space.
Because the Old Space is substantially larger and holds persistent data—such as closures, global variables, and long-lived DOM references—it requires a more thorough collection mechanism known as the Major GC, which primarily uses the Mark-Sweep-Compact algorithm.
Phase 1: Marking (Identifying Reachability)
The marking phase determines which objects are still in use and which can be safely deleted.
- Root Identification: The garbage collector starts with a set of “roots,” including global objects, currently executing stack frames, and DOM trees.
- Tri-Color Marking Algorithm:
- White: Unvisited objects. At the start, all objects are marked white. If an object remains white at the end of the phase, it is considered garbage.
- Grey: Visited objects whose referenced child objects have not yet been evaluated.
- Black: Visited objects along with all their directly reachable outgoing references.
- Graph Traversal: The engine follows pointers from the roots down through the object graph, moving objects from white to grey, and finally to black once fully explored. Once traversal completes, all reachable objects are black, and all unreachable objects remain white.
Phase 2: Sweeping vs. Compacting
Once marking is complete, the engine decides how to reclaim the dead (white) memory.
Sweeping
In standard sweeping, the engine scans the memory space to identify gaps left by dead objects. It records these vacant memory addresses into “free lists.” When new objects are promoted or allocated, the engine consults these free lists to find suitable memory chunks.
Compacting
Over time, repeated allocation and sweeping cause memory fragmentation, where free memory is scattered in small, non-contiguous blocks. This makes allocating large objects difficult, even if total free memory is high.
Compacting addresses fragmentation through the following steps: 1. Relocating Live Objects: The engine shifts surviving (black) objects to one end of the memory page, packing them tightly together. 2. Updating Pointers: Because moving an object changes its physical memory address, the engine updates all incoming references and pointers across the application to point to the new locations. 3. Reclaiming Continuous Space: The remaining space at the end of the page is consolidated into a single, contiguous block of free memory, ready for fast sequential allocations.
Performance Optimizations
Because halting JavaScript execution to perform a full Mark-Compact cycle (a “Stop-the-World” pause) causes UI jank and latency, modern engines employ several optimizations:
- Incremental Marking: The marking phase is broken into small, interleaved steps between normal JavaScript execution tasks.
- Concurrent Marking and Sweeping: Background threads scan and process memory concurrently while the main thread continues executing JavaScript.
- Lazy Sweeping: The engine delays sweeping pages until memory is actually needed by the application, spreading the computational cost over time.