v8引擎不采用引用计数,而是基于代际假说实行分代回收:新生代用scavenge算法复制存活对象,老生代用mark-sweep与mark-compact算法标记清除并整理内存,辅以增量标记和写屏障优化。

V8 引擎**并不实际使用引用计数作为主回收机制**,它仅在极少数内部场景(如某些对象图遍历辅助逻辑)中参考引用关系,但**不依赖引用计数做内存释放决策**。真正主导内存回收的是基于代际假说的**标记清除(Mark-Sweep)及其变种(Mark-Compact)**,辅以新生代专用的 Scavenge 算法。所谓“引用计数和标记清除配合”,其实是早期理论对比,而非 V8 的实际协作流程。
引用计数在 V8 中不被采用的原因
引用计数看似直观,但存在两个硬伤,V8 明确规避:
-
循环引用无法释放:两个对象互相持有引用(如
obj1.ref = obj2; obj2.ref = obj1),即使外部再无引用,计数始终 ≥1,内存永久泄漏 - 性能开销大:每次赋值、重绑定、作用域退出都要同步增减计数,频繁写内存且难以优化
- 无法处理弱引用或跨上下文引用:比如 DOM 节点与 JS 对象间的隐式关联,引用计数无法准确建模
V8 实际采用的分代回收机制
V8 按对象存活时间把堆内存划分为不同区域,每区用最适合的算法:
- 新生代(New Space,约 1–8MB):存放刚创建的对象。使用 Scavenge 算法(类似复制收集)——将存活对象从 From 空间复制到 To 空间,原空间整块清空。快、碎片少,适合短命对象
- 老生代(Old Space):晋升上来、存活较久的对象。主要用 Mark-Sweep(标记可达对象 → 清除不可达对象),再辅以 Mark-Compact(标记后向一端整理,解决碎片问题)
- 大对象空间(Large Object Space):直接分配不移动,避免复制开销;回收时走 Mark-Sweep
标记清除如何真正工作
核心不是“看谁没被引用”,而是“从根出发找谁还活着”:
-
根对象包括:全局对象(
window/globalThis)、当前调用栈中的局部变量、正在执行的闭包环境、DOM 元素引用等 - 标记阶段:GC 从所有根开始广度/深度遍历,对每个访问到的对象打标记(如翻转一个位标识)
- 清除/整理阶段:扫描整个堆,回收未被标记的对象;若启用 Compact,则把存活对象挤到内存起始端,更新所有指针
- 写屏障(Write Barrier):当老生代对象新增指向新生代对象的引用时,V8 会记录该跨代引用,确保下次新生代 GC 时能正确扫描到,避免漏标
为什么开发者仍需理解“引用”概念
虽然 V8 不靠引用计数,但“引用存在与否”直接影响标记结果:
-
let obj = {x: 1}; obj = null;→ 原对象失去唯一引用,下次 GC 标记时不可达,会被回收 -
const el = document.getElementById('box'); el.addEventListener('click', handler);→ DOM 元素和事件监听器互持引用,若忘记removeEventListener或el = null,它们就持续可达,无法回收 -
function outer() { const data = new Array(1000000); return () => console.log(data.length); }→ 闭包让data一直被内层函数引用,即使outer()执行结束也不会释放
本质上,V8 的回收逻辑是保守而可靠的:只要一个对象能被根对象“触达”,它就安全;一旦断开所有路径,哪怕有循环,也会被干净回收。不需要开发者手动计数,但必须清楚自己代码中哪些引用链仍在维持对象存活。










