增量标记通过将长停顿拆为多次微暂停并穿插执行,避免卡顿;它每次仅处理少量对象,在事件循环空闲、帧渲染间隙等时机插入,内存压力大时自动加快节奏但单次仍≤1ms。

增量标记不是让垃圾回收“变快”,而是让它“不集中爆发”。它把原本一次性的长停顿,拆成几十次微小暂停,穿插在 JS 引擎空闲时执行,用户几乎感觉不到卡顿。关键在于理解它的节奏和干扰点——高频对象创建本身不直接触发卡顿,但会加速内存填满、挤压标记窗口,最终迫使引擎放弃增量、回退到长 STW。
增量标记是怎么“拆”和“插”的
它不等堆满了再干,而是边运行边标记:
- 每次只扫描几十到几百个对象,比如从灰色队列里取出一部分,标黑自身、标灰其子对象
- 插入时机很讲究:事件循环空闲时、两次 requestAnimationFrame 之间、甚至一帧渲染刚结束的那几毫秒
- 当内存分配压力上升(比如连续 new 很多对象),标记节奏会自动加快,但单次仍控制在 1ms 内
高频创建为何会破坏增量节奏
高频对象创建本身合法,但会从三方面干扰增量标记的平滑性:
- 快速消耗 Eden 区或新生代空间,频繁触发 Minor GC,打断当前标记任务的连续性
- 大量短命对象涌入,使灰色队列膨胀,后续需更多轮次才能清完,积压后可能合并为一次长标记
- 若伴随 console.log、JSON.stringify 等隐式深遍历操作,会临时增加引用图复杂度,延长单次标记耗时
真正有效的规避策略
目标不是消灭创建,而是让创建“可预测、可复用、少扰动”:
- 用对象池管理高频结构(如粒子、消息体、配置项),borrow/return 替代 new/delete
- 避免在循环中拼接字符串或构造嵌套对象,改用模板字符串或预分配结构体
- 大数组或缓冲区尽量复用(.length = 0 + fill() 或 slice(0)),而非反复 new Array(n)
- 减少闭包捕获大对象,防止本该回收的对象因被闭包引用而滞留至老年代,拖慢全堆标记
怎么确认是不是它在卡你
别猜,看真实行为:
- 打开 Chrome DevTools → Memory → 勾选 “Record allocation stacks”,跑一段高频创建逻辑,看是否集中在某次 GC 后出现长停顿
- 在 Performance 面板录制时启用 “V8 Garbage Collection” 和 “JavaScript stack traces”,观察标记任务是否被长脚本阻塞(如 >5ms 的 for 循环)
- 使用 --trace-gc --trace-gc-verbose 启动 Node.js,日志中若频繁出现 “IncrementalMarkingStep” 后紧跟 “Scavenge” 或 “MarkSweepCompact”,说明增量已失衡

















