标记清除是JavaScript引擎核心垃圾回收算法,通过从根对象出发标记可达对象,再清除未标记对象内存;它解决引用计数无法处理循环引用的问题,现代引擎如V8在老生代使用该算法并辅以增量/并发标记和整理优化。

标记清除(Mark-and-Sweep)是 JavaScript 引擎中最核心的垃圾回收算法,它通过识别“哪些对象还被使用”来决定“哪些内存可以安全释放”,而不是靠计算引用次数。
标记阶段:从根出发,标记所有可达对象
引擎会从一组被称为“根”(roots)的对象开始遍历,比如全局对象(window 或 globalThis)、当前执行上下文中的局部变量、闭包中捕获的变量等。从这些根出发,沿着对象之间的引用关系(如属性、数组元素、闭包引用等)深度或广度优先地访问所有能到达的对象,并给它们打上“存活”标记。
未被标记的对象,意味着没有任何活跃路径能访问到它——即已“不可达”,属于垃圾。
清除阶段:回收未被标记对象占用的内存
在标记完成后,垃圾回收器会扫描整个堆内存,把所有未被标记的对象所占的内存空间释放掉,并通常将空闲内存整理归并,供后续分配使用。
立即学习“Java免费学习笔记(深入)”;
这个过程不移动存活对象(除非配合其他策略如“整理”阶段),但会留下内存碎片;现代引擎(如 V8)常在清除后加入“整理”(Compact)步骤来优化内存布局。
为什么不用引用计数?标记清除解决了它的关键缺陷
引用计数算法简单直观,但无法处理循环引用问题。例如:
let objA = { name: 'A' };
let objB = { name: 'B' };
objA.ref = objB;
objB.ref = objA;
objA = null;
objB = null;
此时两个对象的引用计数仍为 1(彼此引用),但已完全脱离全局作用域——实际已不可达。引用计数算法会错误保留它们,造成内存泄漏。而标记清除只关心是否从根可达,自然能正确回收这类对象。
实际运行中的一些细节
- 标记过程通常是暂停主线程执行的(Stop-the-World),但现代引擎通过增量标记(Incremental Marking)或并发标记(Concurrent Marking)尽量减少卡顿。
- V8 引擎将堆分为新生代和老生代,标记清除主要应用于老生代;新生代常用“Scavenge”算法(基于复制)。
- 全局变量、定时器回调、DOM 事件监听器等若持有对象引用,会阻止其被标记为垃圾——这是前端内存泄漏最常见的成因之一。









