垃圾回收机制(Garbage Collection, GC) 是一种自动化的内存管理技术,用于回收程序中不再使用的内存空间,避免内存泄漏。JavaScript(尤其是 V8 引擎)使用了一些经典的垃圾回收算法,如 标记-清除 和 分代回收。
以下是垃圾回收的原理和具体机制的详细解析:
1. 垃圾回收的核心目标
垃圾回收的主要任务是:
- 找出不再被引用的对象(不可达的对象)。
- 释放这些对象所占用的内存空间。
- 确保程序有充足的内存可以继续运行。
2. 常见垃圾回收算法
(1) 标记-清除算法(Mark-and-Sweep)
这是最基础、最常用的垃圾回收算法。
工作原理:
-
标记阶段(Marking Phase):
- 从根对象(如全局对象、栈中的局部变量)出发,递归检查每个对象是否可以到达。
- 所有可达的对象都会被标记为“活跃”状态。
-
清除阶段(Sweeping Phase):
- 遍历整个内存区域,回收没有被标记的“不可达”对象,并释放它们占用的内存。
示意图:
初始内存状态:
[根对象] --> [对象A] --> [对象B]
[对象C](无引用)
标记阶段:
根对象、对象A、对象B 被标记为“活跃”。
对象C 被标记为“不可达”。
清除阶段:
回收对象C。
优点:
- 简单高效,适合大多数情况。
缺点:
- 垃圾回收期间可能暂停程序执行(称为“停顿”),影响性能。
(2) 分代回收算法(Generational GC)
现代 JavaScript 引擎(如 V8)引入了分代垃圾回收机制,将内存划分为两代:
- 新生代:存放短期存活的对象(如局部变量、临时对象)。
- 老生代:存放长期存活的对象(如全局对象、大型缓存)。
工作原理:
-
新生代和老生代分别使用不同的垃圾回收策略:
- 新生代垃圾回收:采用Scavenge算法,因为新生代中的大多数对象存活时间短,清理时只关注少量存活对象。
- 老生代垃圾回收:采用标记-清除和标记-整理算法,因为老生代中对象存活时间较长,内存碎片化问题更严重。
-
当新生代的对象存活时间足够长时,会被移动到老生代(称为晋升)。
Scavenge算法(用于新生代):
- 将新生代内存分为两个区域:From 空间和To 空间。
- 活跃对象从 From 空间复制到 To 空间。
- 清空 From 空间,交换 From 和 To 空间角色。
(3) 引用计数算法(Reference Counting)
这是另一种垃圾回收方法,通过跟踪对象的引用次数来判断对象是否可以被回收。
工作原理:
- 每个对象维护一个“引用计数”。
- 当有新的引用指向对象时,计数 +1。
- 当引用被移除时,计数 -1。
- 如果计数为 0,回收该对象。
缺点:
- 无法处理循环引用:
let a = {}; let b = {}; a.ref = b; b.ref = a; // 两者引用计数都不为 0,但实际上已无法访问。
由于这个问题,JavaScript 已很少使用引用计数,而采用更强大的标记-清除算法。
3. 垃圾回收的优化机制
(1) 增量回收(Incremental GC)
为了减少标记-清除算法中的全局暂停,增量回收将垃圾回收的过程拆分为多个小步骤,逐步完成垃圾回收。
(2) 并发回收(Concurrent GC)
垃圾回收操作与主线程的代码执行并发进行,进一步减少停顿时间。
(3) 增量标记(Incremental Marking)
在标记阶段分多次完成扫描,避免一次性暂停程序执行。
4. JavaScript 垃圾回收中的 V8 引擎实现
V8 引擎采用了分代垃圾回收机制,结合了标记-清除和增量回收策略:
-
新生代垃圾回收(Scavenge):
- 小对象存活时间短,快速回收。
- 复制存活对象,清空其余内存。
-
老生代垃圾回收(Mark-and-Sweep / Mark-and-Compact):
- 使用标记-清除算法,定期整理内存以减少碎片。
5. 代码实践中的影响
避免内存泄漏的常见情况:
- 未清理的全局变量:
window.globalVar = "I'm here forever";
- 闭包引用:
function outer() { let obj = { key: "value" }; return function inner() { console.log(obj); }; } let closure = outer(); // obj 将一直被引用,不会被回收。
- 事件监听未移除:
const element = document.getElementById("button"); element.addEventListener("click", () => { console.log("Clicked"); }); // 如果 element 被移除但事件监听器未清理,内存无法释放。
优化内存使用的建议:
- 尽量避免创建不必要的全局变量。
- 使用
WeakMap
或WeakSet
存储可能被销毁的对象引用。 - 确保手动移除事件监听器和 DOM 节点引用。
总结来说,垃圾回收机制极大简化了 JavaScript 的内存管理,但了解其原理和局限性有助于编写更高效和稳定的代码。