浏览器的垃圾回收机制

1. 垃圾回收的背景与意义

在现代编程语言中,内存管理是一个核心问题。手动管理内存(如C/C++中的mallocfree)容易导致内存泄漏或野指针等问题。为了解决这些问题,高级语言(如JavaScript)引入了自动垃圾回收机制,由宿主环境(如浏览器引擎)自动管理内存的分配和释放,使开发者可以更专注于业务的开发。

浏览器的垃圾回收机制的目标是:

  • 自动回收不再使用的内存 ,防止内存泄漏。

  • 优化内存使用 ,提高程序性能。

  • 减少开发者的负担 ,让开发者专注于业务逻辑。


2. 内存分配与回收的基本原理

2.1 内存分配

当程序创建对象、字符串、数组等数据结构时,浏览器会从堆内存(Heap)中分配一块内存。堆内存是一个动态区域,用于存储程序运行时创建的所有对象。

2.2 内存回收

当对象不再被程序使用时,垃圾回收器会将其占用的内存标记为“可回收”,因为其开销比较大并且GC时停止响应其他操作,所以垃圾回收器会并在适当的时机释放这块内存。

2.3 关键问题:如何判断对象是否“不再使用”?

这是垃圾回收的核心问题。浏览器通过以下两种主要策略来判断对象是否可以被回收:

  1. 引用计数 :通过统计对象的引用次数来判断。

  2. 标记清除 :通过从根对象出发,遍历所有可达对象来判断。


3. 引用计数算法

3.1 原理

每个对象维护一个引用计数,记录有多少变量或属性引用了它。当引用计数为0时,对象被视为“垃圾”,可以被回收。

3.2 示例

1
2
3
4
5

let a = { name: "Wang" }; // 对象 { name: "Wang" } 的引用计数为 1
let b = a; // 引用计数增加到 2
a = null; // 引用计数减少到 1
b = null; // 引用计数减少到 0,对象可以被回收

3.3 优点

  • 简单高效 :引用计数的增减操作非常快。

  • 实时性 :对象一旦不再被引用,可以立即被回收。

3.4 缺点

  • 循环引用问题 :如果两个对象互相引用,即使它们不再被程序使用,引用计数也不会降为0,导致内存泄漏。
1
2
3
4
5
6
7
8
    
let a = { name: "Wang" };
let b = { name: "Zhang" };
a.friend = b;
b.friend = a;
a = null;
b = null;
// 此时,a 和 b 互相引用,引用计数不为0,无法被回收

由于循环引用问题,现代浏览器主要使用标记清除 算法。


4. 标记清除

4.1 原理

从一组根对象(Roots)出发,遍历所有可达的对象,未被遍历到的对象被视为“垃圾”。

4.2 根对象包括

  • 全局对象 (如window)。

  • 当前执行栈中的变量 (如局部变量、函数参数)。

  • DOM树中的引用

4.3 示例

1
2
3
4
5
let a = { name: "Wang" };
let b = { name: "Zhang" };
a.friend = b;
b = null;
// 此时,a 仍然可以通过 window.a 访问,因此 a 和 a.friend 都是可达的

4.4 优点

  • 可以处理循环引用 :即使对象之间互相引用,只要它们不可达,就会被回收。

4.5 缺点

  • 需要暂停程序 :在遍历过程中,程序需要暂停,可能导致性能问题。

5. 现代浏览器的垃圾回收策略

现代浏览器(如Chrome的V8引擎)采用分代回收 策略,将内存分为新生代和老生代,针对不同区域使用不同的回收算法。

5.1 新生代(Young Generation)

  • 特点 :存放生命周期短的对象(如局部变量)。

  • 算法 :Scavenge算法(一种复制算法)。

    • 将内存分为两个区域:From空间和To空间。

    • 新对象分配在From空间,当From空间满时,将存活对象复制到To空间。

    • 交换From和To空间,清空旧的From空间。

  • 优点 :速度快,适合频繁回收。

  • 缺点 :内存利用率较低(只有一半内存可用)。

5.2 老生代(Old Generation)

  • 特点 :存放生命周期长的对象(如全局变量、闭包引用的变量)。

  • 算法 :标记-清除(Mark-and-Sweep)和标记-整理(Mark-and-Compact)。

    • 标记-清除 :标记所有可达对象,清除未标记的对象。

    • 标记-整理 :在清除后,将存活对象移动到内存的一端,减少内存碎片。

  • 优点 :内存利用率高。

  • 缺点 :速度较慢,可能引起程序暂停。

5.3 增量标记与懒性清理

为了减少垃圾回收对程序的影响,V8引擎引入了以下优化:

  • 增量标记(Incremental Marking) :将标记过程分为多个小步骤,穿插在程序执行中。

  • 懒性清理(Lazy Sweeping) :延迟清理过程,直到需要时再进行。


6. 内存泄漏与优化

尽管有垃圾回收机制,内存泄漏仍可能发生。常见的内存泄漏场景包括:

  1. 意外的全局变量
1
2
3
function foo() {
bar = "This is a global variable"; // 未使用 var/let/const,bar 成为全局变量
}
  1. 未清理的定时器或回调函数
1
2
3
4
5
let data = getData();
setInterval(() => {
console.log(data);
}, 1000);
// 即使不再需要 data,定时器仍然持有引用
  1. 未释放的DOM引用
1
2
3
let element = document.getElementById("myElement");
element.remove(); // 从DOM中移除
// 如果仍然持有 element 的引用,DOM节点无法被回收

优化建议

  • 使用WeakMapWeakSet(本篇不做赘述,没了解过的可以去看看),因为它们对键的引用是弱引用,不会阻止垃圾回收。

  • 及时清理不再需要的引用(如定时器、事件监听器)。

  • 使用开发者工具(如Chrome DevTools)分析内存使用情况,检测内存泄漏。