标记清除算法通过“标记-清除”两阶段回收不可达对象:先从根出发标记所有可达对象,再清除未标记对象内存。它解决循环引用问题,不依赖引用计数,需暂停执行但现代引擎已优化。

标记清除(Mark-and-Sweep)是 JavaScript 引擎中最核心的垃圾回收算法,主要解决“哪些内存可以安全释放”的问题。它的本质不是实时追踪变量生死,而是周期性地识别并回收**不再被任何活跃引用链可达的对象**。
标记阶段:从根出发,遍历所有可访问对象
引擎从一组称为“根”(roots)的全局对象开始(如全局执行上下文中的变量、当前调用栈中的局部变量、浏览器中的 window/globalThis 等),递归遍历所有能通过引用链到达的对象,并给它们打上“存活”标记。
- 每个对象在内存中会有一个隐藏的标记位(如 mark bit),初始为未标记状态
- 遍历过程是深度优先或广度优先,取决于具体引擎实现(如 V8 使用的是增量式标记)
- 函数作用域内的局部变量、闭包捕获的外部变量、DOM 节点引用等,只要还在引用链上,都会被标记
- 注意:只标记“可达对象”,不关心变量是否显式赋值为 null 或 delete,只要引用还存在,就不会被清理
清除阶段:回收所有未被标记的对象内存
标记完成后,引擎扫描整个堆内存,把所有未被标记的对象所占的内存空间释放掉,并通常将空闲内存整理成连续块(部分引擎会配合“整理”或“压缩”步骤,但标准标记清除本身只负责清除)。
- 已释放的对象内存不会立即归还给操作系统,而是由 JS 引擎维护的空闲列表管理,供后续 new / {} / [] 等操作复用
- 清除过程不触发对象的析构逻辑(JavaScript 没有确定性析构函数,finalization registry 是弱补充,非本算法一部分)
- 如果某对象仅被一个已标记对象引用,它也会被标记;但如果所有引用它的对象都未被标记(即不可达),它就会被清除
为什么需要分两步?直接删除不可达对象不行吗
因为无法在不遍历的前提下准确判断“不可达”。引用关系是动态、网状且嵌套的,比如循环引用(A → B,B → A)在引用计数下永远无法释放,而标记清除靠“从根出发的可达性”天然规避该问题。
- 引用计数算法需实时维护计数器,且无法处理循环引用;标记清除不依赖计数,只依赖可达性
- 标记清除必须暂停 JS 执行(stop-the-world)来保证标记一致性——现代引擎通过增量标记、并发标记等方式缓解卡顿
- 它不依赖开发者手动干预,也不要求对象声明生命周期,符合 JS 的自动内存管理设计哲学
实际开发中影响标记清除效率的关键点
虽然你无法直接控制 GC 触发时机,但以下行为会显著影响标记清除的工作量和频率:
- 避免意外的全局变量泄露(如忘记 var/let 声明,导致变量挂到 globalThis 上,成为永久根)
- 及时解除大型数据结构(如缓存 Map、事件监听器、定时器)的引用,尤其在组件卸载或页面跳转时
- 谨慎使用闭包长期持有大对象,闭包作用域链会让内部变量持续“可达”
- 频繁创建短命小对象(如循环内 {} 或字符串拼接)会增加 GC 压力,但现代引擎对此优化较好;真正影响大的是长期驻留的大对象图
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











