V8pedia

Mark-Compact (old generation)

The Mark-Compact collector is V8's major GC. It collects the old generation (and the whole heap), where the Scavenger's copying approach does not fit — old space is large and densely live, so copying everything would be wasteful. Instead it marks the live objects, sweeps the dead space into free lists, and compacts (evacuates) to fight fragmentation. Much of this work is offloaded; see concurrent & incremental marking.

::: info Ubiquitous language Tri-color marking: objects are white (unseen), grey (seen, not scanned), or black (scanned). Sweep: turn dead space into free-list entries. Compact/evacuate: relocate live objects to defragment. :::

The phases

void MarkCompactCollector::CollectGarbage() {
  MarkLiveObjects();

  ClearNonLiveReferences();

  Sweep();
  Evacuate();
  Finish();
}

src/heap/mark-compact.cc#L532-L560

  1. Mark — starting from roots, find every reachable (live) object.

  2. Clear non-live references — clean up weak references whose targets died.

  3. Sweep — reclaim the gaps left by dead objects into free lists for reuse.

  4. Evacuate (compact) — relocate live objects off fragmented pages onto fresh ones, then free the emptied pages, so allocation can stay fast.

  5. Finish — bookkeeping.

The genius is that only Mark and Finish strictly need a main-thread pause; Sweep and Evacuate can run largely concurrently/in parallel.

Marking: tri-color, with roots first

void MarkCompactCollector::MarkLiveObjects() {

  RootMarkingVisitor root_visitor(this);
  MarkRoots(&root_visitor);
  MarkObjectsFromClientHeaps();
  if (v8_flags.parallel_marking && UseBackgroundThreadsInCycle()) {
    parallel_marking_ = true;
    MarkTransitiveClosureFixpoint();   // many threads chase the object graph
    parallel_marking_ = false;
  }

}

src/heap/mark-compact.cc#L2587-L2665

Marking is the transitive closure of "reachable from roots". In tri-color terms: roots go grey; a marker pops a grey object, marks it black, and greys its referents; repeat until no grey remains. Anything still white is dead. Marking can run in parallel (many threads) and concurrently (alongside JS) — covered next page.

The marking bitmap

V8 does not store mark bits inside objects — that would dirty cache lines and fight concurrent readers. Instead each page has a side bitmap, one bit per object-aligned word, with atomic and non-atomic setters:

template <> inline bool MarkBit::Set<AccessMode::ATOMIC>() {
  return base::AsAtomicWord::Relaxed_SetBits(cell_, mask_);
}
template <> inline bool MarkBit::Get<AccessMode::NON_ATOMIC>() const {
  return (*cell_ & mask_) != 0;
}

src/heap/marking.h#L19-L90

Set returns whether it transitioned the bit 0→1 — the atomic version is how concurrent markers avoid double-processing an object without a global lock. Keeping mark state out-of-object is what makes concurrent marking practical: markers touch the bitmap, not the objects' header words that JS is also writing.

Work distribution: marking worklists

Grey objects awaiting scanning live in segmented worklists that threads steal from, balancing load:

using MarkingWorklist = ::heap::base::Worklist<Tagged<HeapObject>, 64>;

src/heap/marking-worklist.h#L27-L65

Segments of 64 objects amortize synchronization (threads grab a whole segment at a time). V8 even keeps per-native-context worklists so it can attribute memory to contexts for the memory-measurement API without slowing marking.

Sweep and compact

After marking, sweeping walks pages turning dead ranges into free-list entries (this can run concurrently — the Sweeper handles quarantined/lazy pages). Evacuation picks fragmented pages and copies their survivors elsewhere, updating all pointers, then releases the empty pages — this is what stops old space from degrading into Swiss cheese over time. Both phases are parallelized.

::: tip Why mark-compact, not just mark-sweep? Pure mark-sweep leaves free space scattered in small holes; over time, allocation either fails to find a fit or must use slow best-fit search, and locality degrades. Compaction periodically relocates survivors together, restoring large contiguous free regions and good cache locality — at the cost of moving objects (which is why handles and write barriers exist). :::

See also