点击在线阅读,体验更好 | 链接 |
---|---|
现代JavaScript高级小册 | 链接 |
深入浅出Dart | 链接 |
现代TypeScript高级小册 | 链接 |
JavaScript引擎的垃圾回收机制
1. 引言
在编程语言中,内存管理是一项关键的任务,尤其对于构建大规模和性能敏感的应用程序来说尤为重要。然而,对于JavaScript这种动态语言来说,开发者通常不需要(也无法)直接管理内存,这项任务主要由JavaScript引擎来完成。
这种自动管理的机制让开发者可以更专注于业务逻辑的实现,而不用担心内存泄漏或溢出等问题。但同时,作为开发者,了解JavaScript引擎如何管理内存,如何进行垃圾回收(Garbage Collection,简称GC),也是很有价值的。这种理解可以帮助我们编写出更高效、更具性能的代码,避免可能导致内存问题的代码模式。
2. JavaScript内存生命周期
在讨论垃圾回收之前,我们首先需要了解一下JavaScript的内存生命周期,这个过程通常分为三个阶段:
-
分配内存:当声明变量、添加属性、或者调用函数等操作时,JavaScript引擎会分配内存来存储值。例如,当你写
let a = 1
时,JavaScript引擎会为变量a
分配一块内存来存储值1
。 -
使用内存:在分配了内存之后,我们可以通过读写操作来使用这块内存。例如,我们可以读取变量
a
的值,或者改变它的值。 -
释放内存:当内存不再被需要时(例如,变量已经离开了它的作用域),这块内存需要被释放,以便为新的内存分配做出空间。这个过程就是垃圾回收。
3. 垃圾回收
垃圾回收是自动完成的。垃圾收集器会周期性地(或在特定触发条件下)运行,找出不再使用的变量,然后释放其占用的内存。但是,如何确定哪些内存“不再需要”呢?这其实是一个复杂的问题,因为某些内存可能仍然被间接引用,或者可能在将来需要。因此,垃圾收集器必须使用一种算法来确定哪些内存可以安全地释放。接下来我们将详细介绍两种常见的垃圾回收算法:标
记-清除算法和引用计数算法。
3.1. 标记-清除算法
这是JavaScript中最常用的垃圾回收算法。它的工作原理大致可以分为两个阶段:标记和清除。
在标记阶段,垃圾回收器从一组“根”(root)对象开始,遍历所有从这些根对象可达的对象。可达的对象包括直接引用的对象,以及通过其他可达对象间接引用的对象。所有可达的对象都被标记为“活动的”或“非垃圾的”。
然后,在清除阶段,垃圾回收器会遍历所有的堆内存,清除未被标记的对象。这些未被标记的对象就是我们所说的“垃圾”,它们无法从根对象访问到,因此我们可以安全地假设它们不会再被应用程序使用。
function test() {
var x = 123;
var y = { a: 1, b: 2 };
// 当函数执行结束时,x 和 y 就离开了环境
}
test();
// 现在 x 和 y 都是非环境变量,它们占用的内存就可以被垃圾回收器回收
3.2. 引用计数算法
引用计数是另一种垃圾回收策略。这种策略的基本思想是跟踪每个对象被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,这个引用类型值的引用次数就是1。如果同一个引用值被赋给另一个变量,引用次数增加1。相反,如果对该值的引用被删除,引用次数减少1。当这个引用次数变成0时,就表示没有任何地方再引用这个值了,因此该值可以被视为“垃圾”并被收集。
然而,引用计数算法有一个著名的问题,那就是循环引用。如果两个对象相互引用,即使它们没有被其他任何对象引用,它们的引用次数也不会是0,因此它们不会被回收,这会导致内存泄漏。为了解决这个问题,现代JavaScript引擎通常会结合使用标记-清除和引用计数两种算法。
function cycleReference() {
var obj1 = {};
var obj2 = {};
obj1.prop = obj2;
obj2.prop = obj1;
}
cycleReference();
// 在函数执行结束后,obj1 和 obj2 仍然相互引用,但已经离开了环境,无法被引用计数器捕获
4. JavaScript引擎的垃圾回收优化策略
现代JavaScript引擎不仅实现了上述的基础垃圾回收算法,而且引入了一些优化策略,以提高垃圾回收的效率并减小对性能的影响。
4.1. 分代收集
大部分的JavaScript对象在创建后很快就会死亡,而那些能活下来的对象,通常能活很久。这给了JavaScript引擎一个优化垃圾收集的思路。它把内存堆分为两个
区域:新生代和老生代。新生代存放的是生存时间短的对象,老生代存放的是生存时间长的对象。
对新生代的垃圾回收采用Scavenge算法,它将新生代的空间一分为二,一个为使用空间(From),一个为空闲空间(To)。新对象总是被分配到From空间,当From空间快被使用完时,就会触发垃圾回收过程。回收过程中,存活的对象将会被复制到To空间,同时From和To空间的角色会对调,也就是原来的To空间变成新的From空间。这个过程称为新生代的晋升策略。
而老生代的对象数量一般较多且存活时间较长,如果还使用上面的Scavenge算法就会占用较多的CPU,因此老生代采用了标记-清除和标记-整理算法。
4.2. 延迟清除和增量标记
为了减小垃圾回收过程对应用程序性能的影响,JavaScript引擎采用了“延迟清除”(Lazy Sweeping)和“增量标记”(Incremental Marking)两种策略。
“延迟清除”是指,在标记-清除算法中,垃圾回收器并不是在标记完对象之后立即清除,而是将清除操作延迟到应用程序空闲时进行。
“增量标记”则是将一次完整的标记过程分解为几个部分,每个部分只标记一部分对象。这样,垃圾回收器可以在运行一小段时间后,暂停一会儿,让出CPU给应用程序,然后再运行一小段时间,如此反复,直到标记所有对象。这种方式可以让垃圾回收和应用程序交替运行,减小了垃圾回收对应用程序性能的影响。
4.3 JavaScript代码优化和垃圾回收
了解了垃圾回收的基本概念和机制后,我们可以通过优化JavaScript代码来减少垃圾回收的压力,提高程序的性能。以下是一些基本的策略:
1.局部变量和立即释放内存
使用局部变量而不是全局变量可以更快地释放内存。这是因为局部变量的生命周期通常比全局变量短,一旦离开了它的环境(例如:函数执行结束),局部变量就可以被标记为垃圾回收。
function test() {
var local = "I'm a local variable";
// 当函数执行结束后,local 就离开了环境,可以被垃圾回收
}
test();
2.解除对象引用
当你不再需要一个对象时,应该解除对它的引用。这样,垃圾回收器在下一次运行时就可以回收这个对象。
var obj = { prop: "I'm an object" };
obj = null; // 现在,obj 可以被垃圾回收
3.避免长生命周期的引用
长生命周期的引用(例如:全局变量或DOM引用)会阻止垃圾回收器回收它们所引用的对象。因此,应该尽量避免使用长生命周期的引用,或者在不再需要它们时及时解除引用。
在理解了JavaScript的垃圾回收机制和如何优化代码以减轻垃圾回收压力之后,我们可以写出更高效、更可靠的代码,从而提高用户体验,降低系统负载。
5. 总结
JavaScript的垃圾回收机制是一个复杂且精妙的系统,它能自动管理内存,让开发者可以专注于实现业务逻辑。虽然大多数时候我们不需要关心垃圾回收的具体过程,但是了解其工作原理,可以帮助我们编写出更高效、更具性能的代码,避免可能导致内存问题的代码模式。