JavaScript 的垃圾回收机制(Garbage Collection, GC)是自动管理内存的核心机制,开发者无需手动释放内存,但理解其原理有助于优化代码性能并避免内存泄漏。
一、垃圾回收的基本原理
-
标记-清除算法(Mark and Sweep)
- 标记阶段:从根对象(如全局对象、活动函数上下文、DOM 节点等)出发,遍历所有可达对象并标记为“存活”。
- 清除阶段:回收未被标记的对象(即不可达对象)占用的内存。
- 特点:V8 引擎默认采用此算法处理老生代内存,但会因遍历整个堆内存导致性能开销
-
引用计数算法(Reference Counting)
- 每个对象维护一个引用计数器,引用数为 0 时立即回收。
- 缺陷:无法处理循环引用(如两个对象互相引用但无外部引用),导致内存泄漏。IE 浏览器曾采用此算法,现代引擎已弃用
二、V8 引擎的优化策略
V8 引擎通过分代管理和增量回收显著提升垃圾回收效率:
-
分代回收(Generational GC)
- 内存分区:
- 新生代(Young Generation):存放短期对象(如函数局部变量),采用 Scavenge 算法(半空间复制法)。
- 老生代(Old Generation):存放长期对象(如闭包、全局变量),采用 标记-清除 或 标记-整理(Mark-Compact) 算法。
- 分代依据:基于“代际假说”(大多数对象生命周期短),减少老生代的扫描频率
- 内存分区:
-
增量回收(Incremental GC)
- 将垃圾回收任务拆分为多个小步骤,穿插在主线程执行中,减少单次停顿时间(Stop-The-World)。
- 通过 写屏障(Write Barrier) 和 记忆集(Remembered Set) 跟踪跨代引用,避免重复遍历老年代
-
Scavenger 算法(新生代回收)
- 三步流程:
- 标记:从 GC Roots 出发,标记新生代存活对象。
- 转移:将存活对象复制到 To-Space,消除内存碎片。
- 指针更新:通过存储缓冲区和记忆集更新旧对象的引用地址。
- 特点:高效处理短期对象,且回收后内存可直接复用
- 三步流程:
三、内存泄漏的常见场景
尽管 JavaScript 自动管理内存,但以下情况仍可能导致内存泄漏:
- 意外的全局变量:未使用
var
/let
/const
声明变量,导致变量始终驻留内存。 - 闭包未释放:闭包持有外部函数作用域的引用,若未主动解除,相关对象无法回收。
- DOM 引用残留:移除 DOM 元素后,JavaScript 仍保留对其的引用(如事件监听器未解绑)。
- 定时器/回调未清除:
setInterval
或setTimeout
未及时清理,持续引用对象
四、开发者优化建议
- 减少全局变量:使用模块化或 IIFE(立即执行函数)限制变量作用域。
- 手动解除引用:在对象不再需要时,将其赋值为
null
(如obj = null
)。 - 避免循环引用:及时清理互相引用的对象(如事件解绑)。
- 监控内存使用:通过 Chrome DevTools 的 Memory 面板分析堆内存快照,定位泄漏点