Everything here is V8 behaviour (Chrome, Node, Deno), described in V8's own blog posts. The spec requires none of it, and other engines differ.
V8 rests on the generational hypothesis: most objects die young. So the heap is split into two generations, collected differently.
Young generation (minor GC, "scavenge"). A small semi-space area split into two halves, only one in use at a time. New objects are allocated in the nursery. When it fills, the scavenger copies the live objects to the other half and the old half is emptied, so only survivors are touched. Per the blog: an object that survives one scavenge becomes intermediate (still young), and one that survives another is promoted to the old generation. Write barriers record old-to-young references so a scavenge need not trace the whole heap.
Old generation (major GC, mark-compact). Triggered as the heap nears a dynamically computed limit. Three phases: marking (tri-colour traversal from the roots), sweeping (freed gaps go on free-lists), and compacting (some fragmented pages are evacuated).
Keeping pauses short (the Orinoco project) uses three techniques:
- Parallel: main thread and helpers split a pause. The scavenger has been parallel since V8 6.2.
- Incremental: the main thread works in small slices.
- Concurrent: helpers work while JavaScript runs; marking is concurrent (Chrome 64, Node 10).
Node knobs: --max-old-space-size (old-generation cap, MiB), --max-semi-space-size, --trace-gc (log each collection). v8.getHeapStatistics() and getHeapSpaceStatistics() show sizes.