标记清除(Mark-and-Sweep)是JavaScript最核心的垃圾回收机制,它从根对象出发标记所有可达对象,再清除未被标记的不可达对象,从而安全释放内存并解决循环引用问题。

标记清除(Mark-and-Sweep)是 JavaScript 引擎中最核心的垃圾回收机制,它通过识别“哪些对象还被使用”来决定“哪些内存可以安全释放”。它不依赖引用计数,因此能正确处理循环引用问题。
标记阶段:从根出发,标记所有可达对象
引擎从一组称为“根”(roots)的对象开始遍历——通常是全局对象(如 window 或 globalThis)、当前执行栈中的局部变量、闭包中捕获的变量等。从这些根出发,递归访问所有能被直接或间接引用到的对象,并给它们打上“存活”标记。
- 比如一个函数内部定义了对象 a,又让 b 引用了 a,而该函数仍在执行中,那么 a 和 b 都会被标记为活跃
- 闭包中保存对外部变量的引用,也会让那些变量在函数返回后仍被标记为可达
- DOM 元素被 JS 变量引用时,也会被标记;但如果 JS 已经释放引用,而 DOM 树本身不再挂载,它就可能进入待清理队列
清除阶段:回收未被标记的对象所占内存
标记完成后,引擎扫描堆内存中所有对象。那些没被标记的对象,说明从根无法到达,已不可访问,于是直接释放其占用的内存空间。
- 这个过程不是“立即”发生——现代引擎(如 V8)会采用增量标记或并发标记,把大任务拆成小块,在空闲帧中执行,避免卡顿
- 清除后不会立即整理内存碎片,但后续分配时可能触发整理(如 V8 的紧凑(compaction)阶段),或交由内存分配器管理
- 某些对象(如带有 finalizer 的 WeakRef)会在清除前触发清理回调,但不保证时机
为什么它能解决循环引用问题?
两个对象互相引用(A → B,B → A),但外部没有任何变量指向它们——这种情况下,它们从根不可达。标记阶段根本不会访问到它们,因此它们会在清除阶段被一并回收。这和引用计数算法(每个对象维护引用次数)有本质区别,后者会因循环导致计数永远不为 0 而泄漏内存。
立即学习“Java免费学习笔记(深入)”;
- 常见例子:事件监听器绑定后又被移除,但监听函数仍被闭包持有 → 若闭包失效,相关对象可被回收
- 手动解除引用(如 obj = null)只是帮助更快进入“不可达”状态,不是必需操作,但对长生命周期对象有优化意义
实际影响与开发者注意事项
你不需要手动触发标记清除,但它会影响性能和内存表现。理解它的行为有助于写出更友好的代码:
- 避免意外保留大对象引用(例如缓存未清理、定时器闭包持有了整个数据结构)
- DOM 节点移除后,确保 JS 里也清空对其的引用(尤其是事件处理器或数据映射)
- WeakMap / WeakSet 不干扰标记过程——它们的键是弱引用,不影响对象是否被标记,适合做元数据存储
- 频繁创建短命对象(如循环中新建对象)压力不大,引擎擅长回收;真正要小心的是长期持有本该释放的引用
不复杂但容易忽略:垃圾回收不是按“时间”或“代码行”运行,而是由内存压力、对象分配速率等触发,标记清除是它背后安静工作的逻辑主干。


















