增量标记是V8自动启用的GC调度策略,将长标记过程拆为穿插在主线程空闲间隙的微任务;它不改变标记-清除本质,依赖可中断性,闭包、定时器等易致其退化为全量标记。

增量标记不是开关,而是调度策略
增量标记(Incremental Marking)不是你调用某个 API 就能开启的功能,它是 V8 引擎在运行时自动启用的 GC 调度机制。它不改变标记-清除的本质,只把原本一次性的长耗时标记过程,拆成多个微任务(microtask-sized chunks),穿插在 JavaScript 主线程的空闲间隙中执行。这意味着:主线程不会被 GC “锁死”几百毫秒,而是每帧渲染前、事件回调之间,悄悄做一点标记工作。
关键点在于“空闲间隙”——V8 会根据当前帧预算(frame budget)估算还能安全花多少时间,再决定本次增量标记的粒度。所以你在 DevTools 的 Performance 面板里看不到一个叫 IncrementalMarking 的独立事件,它被包裹在 MinorGC 或 MajorGC 的子阶段中,表现为多个短小的 Marking 条目。
为什么你写的闭包或定时器会让增量标记失效
增量标记依赖“可中断性”,但某些代码模式会强行延长单次标记时间,甚至退化回全量标记:
-
setTimeout或setInterval持有对大对象的引用,且未 clearTimeout,会导致对象长期不可达却无法被及时标记为垃圾 - 闭包捕获了本该短期存在的大数组或 DOM 节点,而外部作用域又持续活跃,使这些对象滞留在老生代,迫使增量标记反复扫描同一块内存
- 频繁调用
JSON.stringify或创建大量临时字符串,触发新生代快速填满,连带引发更激进的老生代标记压力
这些情况不会让增量标记“报错”,但它会显著拉长总标记耗时,甚至让引擎判定“不如一次清完”,从而放弃增量策略,回归 Stop-the-World 标记。
如何在 Chrome DevTools 中确认增量标记是否生效
打开 DevTools → Performance 面板 → 开始录制 → 执行一段已知会触发 GC 的操作(如连续创建 10000 个对象后手动调用 gc(),需在命令行启用 --js-flags="--expose-gc")→ 停止录制:
- 展开某次
MajorGC事件,查看其子项:若看到多个离散的Marking阶段(间隔几毫秒,持续时间均 - 若只看到一个长达 20ms+ 的
Marking子项,大概率是引擎因压力过大跳过了增量逻辑 - 注意对比
Marking和Sweeping的耗时比例——增量只作用于标记阶段;清除阶段仍是同步的,但通常很轻量
别依赖 Timeline 中的“GC Event”图标判断:那个图标只表示 GC 启动,不反映内部是否增量。
增量标记对开发者的真实影响边界在哪
它缓解的是“单次停顿”,不是“总开销”。哪怕所有标记都增量完成,如果应用每秒生成 50MB 临时对象,V8 仍要高频调度增量任务,主线程依然会被频繁打断——只是每次打断从 50ms 缩短到 2ms 而已。真正决定体验的,是你代码中对象的生命周期是否匹配分代假设(YoungGeneration 短期、OldGeneration 长期)。
比如在 React 组件中用 useMemo 缓存一个 10MB 的解析结果,这个对象几乎立刻晋升到老生代;后续任何对它的引用变更,都会让增量标记反复扫描这块固定内存。这种场景下,优化方向从来不是“怎么调参让增量更高效”,而是“这个缓存是否真有必要存在”。


















