--max-old-space-size
5,000 iterations · 200 entries ×
· x-axis: flag value (effective
heap_size_limit in parentheses)
| Chart | What to look for |
|---|---|
| 1 GC share (%) | At 64 MB with a ~20 MB working set, MajorGC alone consumes ~55 % of elapsed time. The fraction drops steeply through 512 MB and plateaus near zero beyond 1–2 GB, where MinorGC dominates and MajorGC barely fires. Switch to the smaller entry size to see this effect compress; switch to the larger to see even more aggressive GC pressure at small heaps. |
| 2 GC total time (ms) | MajorGC total collapses as the heap grows (high at 64 MB, near zero at ≥1 GB). MinorGC total rises in the opposite direction: with a large heap the young generation fills up without MajorGC intervening, forcing more minor scavenges. |
| 3 GC event count | MajorGC fires dozens of times at 64 MB and approaches zero at ≥1 GB; IncrementalGC at 128+ MB tracks MajorGC 1:1. At 64 MB many major GCs are triggered by allocation failure rather than the incremental scheduler, so IncrementalGC count is lower. MinorGC count increases steadily with heap size. |
| 4 Avg time / event (ms/event) | Counter-intuitively, each individual MajorGC event takes longer as the heap grows (a few ms at 64 MB, tens of ms at multi-GB sizes). With a large heap V8 commits more address space and has more internal metadata to process per cycle. The net overhead is still lower because events are far less frequent. |
A 200-entry circular cache keeps the working set alive in old generation. With a small heap the working set occupies a large fraction of available space, so V8 runs Major GC (MarkCompact via incremental marking) aggressively. With a large heap the same working set is negligible — Major GC barely fires.
Modern V8 routes all Major GC through the incremental marking pipeline,
so the stop-the-world MarkCompact phase shows up as
MajorGC (kind=4), not
ConstructRetained (kind=2). Each MajorGC is paired with a
corresponding IncrementalGC event (kind=8) for the
preceding marking phase.
The 2× working-set rule. When the heap is smaller than
roughly 2× the working set, MajorGC fires at extreme frequency (100–130
events per run), consuming 40–70 % of elapsed time. Once the heap
exceeds 2–3× the working set, MajorGC event count drops to single digits
and total GC overhead settles into the 30–40 % range. Set
--max-old-space-size to at least 2× the old-generation
working set; 3× provides a comfortable margin.
The ≈ 30–35 % MinorGC floor. Even with an arbitrarily large heap, total GC overhead does not approach zero — it plateaus around 30–35 % of elapsed time, driven entirely by MinorGC. A larger heap allows more objects to accumulate in old generation without triggering MajorGC, so new-space fills up faster and minor scavenges become more frequent. Reducing this floor requires lowering the object allocation rate, not increasing heap size.
Latency vs. throughput trade-off. Increasing heap size reduces total GC overhead (throughput benefit) but raises the duration of individual MajorGC pauses (latency cost). For latency-sensitive services, infrequent but long stop-the-world pauses at multi-GB heaps can be more harmful than frequent short pauses at a smaller heap. If single-pause latency matters, consider sharding the workload across more processes so each process’s working set stays small.
| Kind | Name | What it is |
|---|---|---|
| 1 | MinorGC |
Scavenger on the young generation (new space). Fast; promotes survivors to old generation. |
| 2 | ConstructRetained |
Stop-the-world MarkCompact without incremental
marking. Rare in normal runs; appears when GC is forced
(e.g. global.gc() with
--expose-gc).
|
| 4 | MajorGC |
Full old-generation collection (MarkCompact) with incremental marking. This is the dominant cost under heap pressure. |
| 8 | IncrementalGC |
One incremental marking step. V8 slices marking work across many small pauses between JS execution turns. |
| 16 | WeakCB |
Processing weak references and their finalizer callbacks after a GC cycle. |
| 32 | AllExternalMemory |
GC triggered to account for externally allocated memory (e.g.
large Buffer / ArrayBuffer backing
stores) that exceeded a threshold.
|
| 64 | ScheduleIdle |
Idle-time GC — V8 schedules a collection when the event loop has spare time, to amortise future pressure. |