JavaScript垃圾回收机制:原理、算法与V8优化详解
本文深入解析JavaScript垃圾回收机制,从可达性概念出发,详细阐述标记清除法与引用计数法的原理及优缺点。重点介绍V8引擎采用的分代回收策略,包括新生代的复制算法与并行回收,以及老生代的标记整理、增量标记、惰性清理和并发回收优化,帮助开发者理解内存管理以提升应用性能。
1. 垃圾为何要产生并回收
编写代码时,无论是创建基本类型、对象还是函数,都会占用内存资源。在JavaScript中,基本数据类型通常存储在栈内存中,而引用数据类型则存储在堆内存中。需要注意的是,栈内存中会保存指向堆内存中实际对象的引用地址。
例如,若我们实例化一个person实体,随后将person实体重新赋值:
var person = {
name: "橘猫吃不胖",
age: 2
}
person = [1, 2, 3];
console.log(person); // [ 1, 2, 3 ]
此时,堆内存中原本为person对象分配的空间依然存在,栈内存中保存着指向该对象的引用地址。但在后续操作中,person被重新赋值为一个数组,这意味着栈内存中的引用地址从原来的对象指向了新的数组。由于原有的引用关系断裂,原本堆内存中为该对象分配的空间便成为了无法访问的垃圾数据。

如果产生的垃圾数据大量堆积且未被及时清理,将严重消耗系统内存,导致性能下降,甚至可能引发系统崩溃。
2. 垃圾回收机制
JavaScript内存管理的核心概念是“可达性”(Reachability)。
那么,什么是可达性呢?假设我们定义一个对象:
let person = {
name: "橘猫吃不胖",
age: 2
}
console.log(person.name, person.age); // 橘猫吃不胖 2
变量person引用了这个对象。通过person.name可以获取到“橘猫吃不胖”的值,通过person.age可以获取到数值2。在这种情况下,我们可以认为“橘猫吃不胖”和数值2是“可达”的,因为它们可以通过变量访问到。
person = null;
console.log(person.name, person.age); // TypeError: Cannot read properties of null (reading 'name')
如果将person设置为null,那么这两个值将无法再通过该变量访问,它们变成了“不可达”状态。此时,JavaScript的垃圾回收机制会自动介入,从内存中清除这些不可达的对象。
JavaScript垃圾回收的核心任务就是定期找出这些不可达的对象并将其释放。目前主要有两种常用的策略来识别不可达对象:
- 标记清除法
- 引用计数法
2.1 标记清除法
标记清除法(Mark-Sweep)分为两个阶段:标记和清除。在标记阶段,垃圾回收器从根节点(如全局对象、当前执行栈等)开始遍历内存中的所有对象,并为所有可达的对象打上标记。在清除阶段,回收器会销毁所有未被打上标记的对象(即非可达对象),并回收其占用的内存。
标记清除法的主要优点是实现相对简单。
然而,它存在两个主要缺点。首先是内存碎片化问题。由于清理垃圾后,未被清除的对象在内存中的位置保持不变,而被清除对象留下的空白空间分散在已用内存中,导致内存碎片化。

第二个缺点是内存分配速度可能变慢。由于空闲内存不再是一块连续的大区域,当需要分配大小为size的新对象时,系统必须遍历空闲内存链表,寻找一块足够大(大于等于size)的连续空间进行分配,这增加了分配开销。

标记清除算法改进—— 标记整理算法
针对标记清除法导致的内存碎片化问题,可以使用标记整理算法(Mark-Compact)进行改进。
标记整理算法的标记阶段与标记清除法相同,即从根节点遍历所有对象并标记可达对象。但在标记结束后,它会将所有存活的对象移动到内存的一端,然后清理掉边界之外的内存。这种方式消除了内存碎片,但移动对象的过程可能带来额外的性能开销。

2.2 引用计数法
引用计数法(Reference Counting)通过记录每个对象被引用的次数来进行垃圾回收。如果某个对象的引用次数为0,说明它不再被使用,垃圾回收机制会立即将其清理。
示例代码如下:
let person = { name: "橘猫吃不胖" }; // { name: "橘猫吃不胖" } 引用次数为1
let person1 = person; // { name: "橘猫吃不胖" } 引用次数为2
person = null; // { name: "橘猫吃不胖" } 的引用次数为1
person1 = null; // { name: "橘猫吃不胖" } 的引用次数为0
引用计数法的优点是可以实现立即回收。当对象的引用计数降为0时,内存会被立即释放,无需等待特定的回收周期。
但它也有两个主要缺点。首先,引用计数本身需要占用额外的存储空间来维护计数器,且计数器的管理可能带来性能开销。
第二个缺点是它无法处理循环引用(Circular References)问题。例如,a对象引用了b对象,同时b对象也引用了a对象,且这两个对象相互引用,导致它们的引用计数始终不为0。即使外部不再引用这些对象,垃圾回收机制也无法清理它们,造成内存泄漏。如下所示:
let a = { name: "橘猫吃不胖" };
let b = { age: 2 };
a.age = b;
b.name = a;
3. V8对垃圾回收机制的优化——分代式垃圾回收机制
目前大多数浏览器引擎基于标记清除算法进行优化,而V8引擎采用了更高效的“分代式垃圾回收机制”。
3.1 新生代与老生代
传统的垃圾回收机制在每次回收时都需要检查内存中的所有对象。然而,内存中的对象具有不同的生命周期:一些是大对象、老对象或存活时间长的对象,而另一些是小对象、新对象或存活时间短的对象。对所有对象采用相同的检查频率显然效率低下,因为老对象并不需要频繁清理。因此,V8引入了分代式垃圾回收机制。
V8将堆内存划分为“新生代”和“老生代”两个区域,并采用不同的回收策略。新生代主要存放存活时间较短的对象,容量通常较小,约为1MB至8MB;老生代则存放存活时间较长或常驻内存的对象,容量较大。V8整个堆内存的大小等于新生代与老生代内存之和。

3.2 新生代的垃圾回收
在新生代垃圾回收策略中,堆内存被划分为两个部分:一部分是处于使用状态的“使用区”(From Space),另一部分是处于闲置状态的“空闲区”(To Space)。

新创建的对象通常被分配到使用区。当使用区快满时,系统会执行一次垃圾回收。新生代垃圾回收机制会对使用区中的活动对象(即存活对象)进行标记,然后将这些活动对象复制到空闲区,并在复制过程中进行排序以避免内存碎片化。最后,清空使用区,并将原来的空闲区变为新的使用区,原来的使用区变为空闲区。

如果一个对象经过多次复制后依然存活,它将被视为生命周期较长的对象,从而被移动到老生代内存中。此外,如果一个对象在复制到空闲区时,发现空闲区的占用空间超过了25%,该对象也会直接进入老生代内存。
新生代回收策略——并行回收
JavaScript是单线程语言,在执行垃圾回收时,通常会阻塞JavaScript脚本的执行,这种现象称为“全停顿”(Stop-The-World)。
如果一次垃圾回收耗时100ms,那么脚本执行也必须暂停100ms。若垃圾回收时间过长,会导致页面卡顿,严重影响用户体验。为了解决这一问题,可以采用并行回收策略。
并行回收是指在主线程进行垃圾回收的同时,开启多个辅助线程共同参与回收过程。这就好比一项任务一个人需要30天完成,如果安排多人协作,可能只需10多天甚至更短时间即可完成。通过并行回收,可以显著降低垃圾回收导致的暂停时间。
新生代对象空间采用并行策略。在执行垃圾回收时,系统会启动多个线程负责新生代中的垃圾清理操作。这些线程同时将对象空间中的数据移动到空闲区域。由于数据移动会导致地址变化,系统还需要同步更新指向这些对象的指针,这一过程即为并行回收。

3.3 老生代的垃圾回收
老生代的垃圾回收主要基于标记清除算法的步骤:在标记阶段标记所有可达对象,在清除阶段销毁未标记的对象。鉴于标记清除法可能产生内存碎片,V8使用标记整理算法来优化这一过程。
老生代回收策略——增量标记与惰性清理 ①增量标记
增量标记(Incremental Marking)将一次完整的标记过程分割为多个小步骤。每执行完一个小步骤,就允许JavaScript应用逻辑执行一段时间,如此交替进行,最终完成垃圾回收。但这引入了两个新问题:如何暂停标记过程以执行JavaScript代码,以及如果在标记过程中对象的状态发生变化(从可达变为不可达,或反之)该如何处理?V8分别通过“三色标记法”和“写屏障”来解决这两个问题。

a.三色标记法
三色标记法使用三种颜色来标记对象的状态:白色表示初始状态(未被访问),黑色表示已检查状态(完全处理完毕),灰色表示待检查状态(已被访问但子对象未完全处理)。
其执行过程如下:
1. 将所有对象初始化为白色,从根节点(Root)出发,将所有直接可达的对象标记为灰色,并将它们放入一个队列中;
2. 遍历该队列,每次取出一个对象将其标记为黑色并移出队列,同时将其相邻的未访问对象涂成灰色并加入队列,直到队列为空;
3. 继续检查是否有灰色对象,如果有则继续将其加入队列并循环处理,直到所有可达对象都变为黑色。
采用三色标记法后,当程序恢复执行时,系统可以直接判断内存中是否存在灰色节点。如果存在灰色节点,则从该节点继续执行标记过程;如果没有,则直接进入垃圾清理阶段。
b.写屏障
写屏障(Write Barrier)用于解决标记过程中对象状态变更的问题。如果在执行JavaScript代码时,内存中标记好的对象引用关系被修改(例如,一个黑色对象引用了一个白色对象),写屏障会将该白色对象重新标记为灰色,以确保在下一轮标记中能正常处理该对象,从而保证回收的正确性。
②惰性清理
增量标记完成后,开始执行垃圾清理。如果当前可用内存足以支持代码的快速执行,则无需立即清理所有内存。此外,清理过程也不必一次性完成,可以根据需要逐步进行。
优点:大大减少了主线程停顿的时间,使浏览器与用户的交互更加流畅。
缺点:虽然减少了单次停顿时间,但并未减少主线程的总暂停时间,甚至可能因多次小停顿而略微增加总耗时。
老生代回收策略——并发回收
并发回收(Concurrent Marking)是指主线程在执行JavaScript代码的同时,辅助线程可以在后台完成垃圾回收操作。在辅助线程执行垃圾回收期间,主线程可以自由执行JavaScript代码,两者互不阻塞。

垃圾回收机制多次阅读之后,我受益匪浅,因此写该文章记录一下~
到此这篇关于JavaScript垃圾回收机制原理总结深入探究的文章就介绍到这了,更多相关JavaScript垃圾回收内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
怎么使用 JavaScript import() 实现模块的条件加载
2026-11-05 14:44:45
前端防御XSS攻击:怎么防止恶意JS注入与CSP策略配置
2026-11-04 08:51:38
怎么解决 Turf.js 处理大规模 GeoJSON 数据的性能瓶颈
2026-11-03 23:22:19
为什么不建议在JS中手动拼接JSON字符串?
2026-11-03 22:48:27
代码混淆对JS执行性能的影响测试与分析
2026-11-03 22:25:37
