mark-compact不是单纯清理垃圾,而是解决内存碎片、腾出连续大块空间;它先标记存活对象,再紧凑迁移至堆起始端,最后清理尾部空间,虽需更新引用、开销较大,但能保障连续内存分配。

老生代中的标记整理(Mark-Compact)不是单纯“清理垃圾”,而是为了解决内存碎片、腾出连续大块空间;晋升策略则决定了哪些对象有资格进入老生代——它不看创建时间,而看实际存活行为和大小特征。
Mark-Compact 的核心作用是物理整理内存
老生代堆空间大(Node.js 默认约 1.4 GB),对象生命周期长。若只用 Mark-Sweep(标记-清除),回收后容易留下大量零散小空闲块。当后续需要分配一个较大对象(比如 512KB 的 ArrayBuffer)时,即使总空闲量足够,也可能因无连续空间而失败或被迫提前触发 GC。
Mark-Compact 分三步解决这个问题:
- 先像 Mark-Sweep 一样,从根(全局变量、栈帧、DOM 引用等)出发,递归标记所有可达对象;
- 再把所有已标记的存活对象,统一往堆内存起始地址方向紧凑迁移,彼此紧挨着排列;
- 最后将迁移后空出的尾部大片内存设为空闲区,供下一次快速分配使用。
这个“移动”动作带来关键影响:对象内存地址会变,V8 必须同步更新所有指向它的引用(如栈中局部变量、其他对象的属性),所以开销比 Mark-Sweep 大。但换来的是更少的 GC 频次和更稳定的分配性能。
晋升不是自动升级,而是按条件“搬仓库”
对象刚创建时默认进入新生代,能否晋升到老生代,取决于两个硬性条件:
- 经历两次 Scavenge 回收后仍存活——说明它大概率会长期存在;
- To 空间剩余容量不足(例如已占超 25%),V8 会提前把部分存活对象晋升,防止复制失败。
另外,大对象(如超过 2MB 的 ArrayBuffer、长字符串)一创建就跳过新生代,直入老生代,避免复制开销。晋升过程对 JS 代码完全透明:对象数据被移动,引用关系由引擎自动修正。
你的代码会间接影响 Mark-Compact 的效率
Mark-Compact 不直接分析代码,但它处理的对象集合,完全由你的引用管理决定——哪些对象被标记为“存活”,就由哪些变量或结构持续持有它们。
- 未清理的事件监听器(绑了没 removeEventListener);
- 闭包中意外捕获并长期持有大型数组、DOM 节点或缓存对象;
- 漏写 var/let/const 导致变量挂到全局;
- 定时器或 Promise 回调中持续引用上下文对象。
这些都会让本该回收的对象被“根”间接引用,被迫参与每次 Mark-Compact —— 搬得越多,主线程停顿越久,页面卡顿越明显。
它不会每次都运行,只在必要时启动
V8 对老生代 GC 很克制:先尝试轻量级的 Mark-Sweep;只有当检测到内存碎片严重(比如最大连续空闲块远小于已用内存的 10%),或老生代使用率持续高于阈值(如 70%),才会触发 Mark-Compact。
现代 V8 还支持 Parallel Compaction(并行整理),多个线程协同搬运对象,大幅缩短单次停顿时间。不过,是否启用、何时启用,均由引擎根据内存压力和运行负载动态决策,开发者无法手动触发。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











