MajorGC (Mark-Compact)Each chart varies ONE parameter; y-axis = MiB allocated when the first major GC fires. Workload: ~1 MiB arrays into old space, ~40 MiB live sliding window. Each point = median of 5 runs.
--initial-old-space-size--max-old-space-size--gc-global off vs on| Chart | What it shows |
|---|---|
1 --initial-old-space-size |
Sets the old-space capacity V8 starts with. Below ~128 MiB the
trigger is floored at ~64 MiB (V8's growth-factor heuristic over
the live set); from 128 MiB up the flag sets the moment directly
and monotonically (128→80, 256→160, 512→256, 1024→480 MiB).
default lands at the 512 MiB point.
|
2 --max-old-space-size |
The old-space cap. A tight cap pulls the trigger earlier (64→54, 128→64 MiB); as it relaxes the moment climbs and saturates at the natural default trigger (~304 MiB) once the cap no longer binds. |
| 3 object size | Larger allocations delay the first major GC (64 KiB→17 MiB, 1 MiB→304, 8 MiB→456). Bigger chunks mean fewer V8 allocations per MiB, so old space fills and hits its trigger more slowly in cumulative-byte terms. |
4 --gc-global |
Forcing every GC to be global collapses the first major GC to ~1 MiB regardless of object size — it bypasses the heuristic entirely. The flat aqua bar vs the climbing blue bar shows the override. |
The workload allocates ~1 MiB arrays (large enough to land in old
generation) and retains the last ~40 MiB of them in a sliding window;
older chunks are released, so old space holds live data plus dead
promoted objects. A PerformanceObserver watches
gc entries; the first MajorGC (kind = 4,
Mark-Compact) is located by correlating each entry’s
startTime against a per-iteration timeline, so the reported
moment and before/after heap are real — not the nearest event-loop
yield.
V8’s major-GC trigger scales with old-space capacity:
--initial-old-space-size sets the starting capacity
(chart 1), --max-old-space-size sets the cap (chart 2). The
first minor GC (Scavenge, young generation) fires early and
often — V8’s new space is small and fills quickly, copying survivors to
old space. It is fixed by V8’s minimum semi-space and is
not moved by these flags — only the first major GC is.
--gc-global bypasses the heuristic and forces every GC to
be major, so the very first GC is a Mark-Compact.
The sliding window (~40 MiB live) lets the major GC actually reclaim
dead promoted objects — visible as the before → after
used drop in the CLI output — instead of retaining
everything until the heap limit.
This report is generated by generate-report.js. The same
detection runs live in index.js, which prints the first
major GC’s kind, duration, iteration, before/after heap, the minor-GC
count before it, and a one-line “why”.
# default — first major GC fires at V8's reclamation trigger
node first-gc/index.js
# smaller initial old space → first major GC fires much earlier
node --initial-old-space-size=4 first-gc/index.js
# larger initial old space → first major GC fires later
node --initial-old-space-size=64 first-gc/index.js
# force every GC to be global → first GC is a Mark-Compact at ~1 MiB
node --gc-global first-gc/index.js
# keep allocating, print each major GC (up to 5) with minor-GC counts between
node first-gc/index.js --keep-going
| Flag | Effect on the first major GC |
|---|---|
--initial-old-space-size=N (MiB) |
Initial old-space capacity → sets the early trigger (has a floor). Most direct. |
--max-old-space-size=N (MiB) |
Old-space cap; a tight cap pulls the trigger earlier, saturates at default once it stops binding. |
--gc-global |
Forces every GC to be major → first GC is a Mark-Compact at ~1 MiB, overriding all heuristics. |
--gc-interval=N |
Force a GC every N V8 allocations (mainly affects minor-GC cadence, not the major trigger). |
--expose-gc |
Exposes global.gc() for manual triggering. |
Script option: --keep-going continues past the first major
GC and prints each subsequent major GC (up to 5), with the minor-GC
count between them.
Two levers, one override.
--initial-old-space-size and
--max-old-space-size both move the first major GC, but in
different regimes: the initial size sets the early trigger (and has a
floor), the max size sets the ceiling that, when tight, pulls the
trigger earlier. --gc-global overrides both, forcing a
major GC almost immediately — useful for forcing reclamation, costly for
throughput.
Allocation shape matters. Chart 3 shows the first major GC is not a fixed property of the heap — larger per-allocation objects push it later. When estimating when a service will first major-GC, the object size distribution matters as much as the heap flags.