JavaScript 的垃圾回收是自动进行的,一般情况下,无需开发者去手动 GC。
你可能会好奇 JavaScript 是如何做到自动回收的?什么情况下变量不会被回收?我们编写代码的时候需要注意什么?
1、算法
垃圾回收有很多种算法,我们一一介绍
1.1 标记清除(Mark-and-Sweep)
标记清除算法:在程序运行时间中定期扫描内存中的对象,标记那些不再使用的对象,然后清除这些标记的对象。
弊端:
- 时间开销:因为在程序运行时间中需要定期扫描内存中的对象,标记那些不再使用的对象,然后清除这些标记的对象,所以会带来一定的时间开销。
- 空间开销:在扫描内存对象的过程中,需要为每个对象额外分配一些空间来存储标记信息,这样会带来一定的空间开销。
- 整理碎片:因为标记清除算法是通过标记某些对象来进行回收,所以会产生空间碎片,这些碎片可能会影响程序的性能。
- 不能处理循环引用:标记清除算法只能处理那些不再使用的对象,如果存在循环引用的情况,可能会导致一些对象不能被正确回收。
1.2 引用计数(Reference Counting)
引用计数算法:每个对象都有一个引用计数器,当有变量或对象指向它时,该对象的计数器就会增加;当没有变量或对象指向它时,该对象的计数器就会减少。如果一个对象的计数器为 0,那么它就会被垃圾回收机制回收。
弊端:
- 复杂度:引用计数算法需要维护每个对象的引用计数器,每次对象引用关系发生变化时都需要更新计数器,这会带来较高的复杂度。
- 无法处理循环引用:如果两个对象相互引用,但是都不再被使用,由于计数器都不为0,于是都不会被回收,这就会导致内存泄漏。
- 无法处理闭包:当一个闭包中的变量不再使用时,对应的计数器不会变为0,这样就会导致闭包中的变量不能被回收。
- 高开销:引用计数算法会对性能产生很大的开销,因为要不断的跟踪每个对象的引用关系。
1.3 可达性分析算法
可达性分析算法是一种基于标记清除的算法,它会在运行时间中扫描内存中的对象,标记那些可达的对象,然后清除那些不可达的对象。
注意:这种算法可以有效的防止内存泄漏,但会带来一定的性能开销。
1.4 分代回收算法
分代回收算法是一种垃圾回收算法,它将堆中的对象分成不同的代,每个代都有不同的回收策略。
新生代代表着新创建的对象,它们很可能很快就会被回收,所以新生代对象会被更频繁地扫描,并且当它们被标记为不可达时会被立刻回收。
而老生代代表着长期存在的对象,它们可能会持续存在很长时间,所以老生代对象会在运行时间长的情况下才会被回收。
分代回收算法的优点是可以更高效地回收内存,并且可以避免对短命对象进行过多的扫描和回收,提高性能。
通常分代回收算法都是基于标记-清除算法或标记-整理算法来实现的。
分代回收算法的一个缺点是需要额外的空间和时间来维护不同的代,并且在高负载下可能会导致更多的停顿。
总结来说, 分代回收算法是一种将堆中的对象分成不同的代,根据对象的存活时间来进行回收的算法,目的是提高回收的效率和性能。
2、回收时机
现代 JavaScript 的运行环境采用的是基于标记清除算法的垃圾回收机制,而且为了减少这种算法带来的性能开销,运行环境会在合适的时机进行垃圾回收,例如在程序执行过程中 空闲时间 进行垃圾回收,例如在程序执行时间过长或内存使用率过高时进行垃圾回收。
在运行环境中,垃圾回收算法会监测内存使用情况,当内存不足时会触发回收。
所以,当一个变量被标记清除时,它不是立刻被回收的,垃圾回收器会在运行时检查变量和对象的可达性,并在适当的时候回收不再使用的内存。这称为垃圾回收的延迟,因此程序员不需要关心垃圾回收的时间点。
3、GC 现状
3.1 不同浏览器的实现
每个浏览器都有自己的 JavaScript 引擎和垃圾回收机制
- Google Chrome 浏览器使用 V8 引擎,它采用了增量标记清除算法和分代回收算法来进行垃圾回收
- Mozilla Firefox 浏览器使用的是 SpiderMonkey 引擎,它采用了增量标记清除算法来进行垃圾回收
- Microsoft Edge 浏览器使用 Chakra 引擎,它采用了标记-清除和引用计数算法结合的垃圾回收机制,并采用了分代回收的思想(2020年8月被微软抛弃,采用 Chromium 内核)
- Safari 浏览器使用了 JavaScriptCore 引擎,它采用了标记-清除算法来进行垃圾回收
所以,每个浏览器都有自己的 GC 回收机制,它们在实现上可能略有不同,但都是为了解决同样的问题:自动回收不再使用的内存。
3.2 Chrome V8 两种算法交织
Chrome V8 使用增量标记清除算法来回收新生代对象,并使用分代回收算法来回收老生代对象。
新生代对象会被更频繁地扫描,并且当它们被标记为不可达时会被立刻回收,而老生代对象则在运行时间长的情况下才会被回收。这样做的目的是避免对短命对象进行过多的扫描和回收,提高性能。
总结来说, Google Chrome 浏览器使用的是 V8 引擎,它采用了增量标记清除算法和分代回收算法结合的垃圾回收机制。新生代对象采用增量标记清除算法回收,而老生代对象则采用分代回收算法回收。这样做的目的是为了提高回收效率和性能。
需要注意的是,V8引擎还采用了一些其他优化技术,比如说空间整理,这样可以减少空间碎片,更好地利用内存空间。
总之,V8的垃圾回收机制是一个非常复杂的系统,结合了多种算法和优化技术来实现高效的内存回收。
3、名词解释
空间碎片:
空间碎片是指在内存管理过程中由于垃圾回收机制的影响,造成的连续空间被分割成若干小的块,这些小的块称为空间碎片。这些空间碎片可能导致后续内存分配不能满足需求,降低程序性能。
空间碎片主要由基于标记清除算法的垃圾回收机制产生,因为这种算法会标记某些对象来进行回收,所以会产生空间碎片。空间碎片可能会影响程序的性能,因此需要进行整理。
举个例子: 假设当前程序使用了 100MB 的内存,其中有一块连续的 50MB 的空间被分配给了一个大对象。然后这个大对象被标记为不再使用,垃圾回收机制进行回收,释放了这 50MB 的空间。但是由于这块空间较大,可能不能被分配给小对象。于是这块空间就成为了一个空间碎片。 如果后续程序需要再次分配内存,但是这块空间碎片可能不能满足需求,那么程序就会继续分配新的内存,这样会导致内存碎片化,影响程序性能。
如果这个大空间碎片不去清理,那么就会导致这个程序占用内存越来越高
或许你回问,为什么大空间碎片不能给小对象?主要原因有以下几点:
- 内存对齐:空间碎片的大小可能不能满足系统的对齐要求,而小对象的内存分配需要满足对齐要求。
- 碎片管理:空间碎片可能不能被碎片管理算法进行整合和重新分配,因此不能分配给小对象。
- 空间分配策略:空间碎片可能并不能满足程序当前的空间需求,而小对象的内存分配需要满足程序的需求。
闭包:
闭包是指一个函数及其相关引用环境组成的包裹。在 JavaScript 中,当一个函数在另一个函数的作用域内被定义时,就会形成闭包。
闭包具有三个特征:
- 闭包可以访问它被定义时所在的作用域中的变量。
- 闭包可以访问它自己的参数和变量。
- 闭包可以被保存到变量中,并在稍后调用。
闭包的一个重要用途是封装私有数据和状态,它可以让你在不暴露实现细节的情况下提供封装的对象。它还可以用于编写模块化的代码。
在 JavaScript 中,闭包的作用域是保存在它被定义时的上下文中的,它可以访问到所有在该上下文中可以访问到的变量。这意味着,闭包可以访问它所在函数的作用域中的变量,以及它所在的全局作用域中的变量。
例如:
代码语言:javascript复制function outerFunction(x){
var innerVar = 1;
function innerFunction(y){
return x y innerVar;
}
return innerFunction;
}
var closure = outerFunction(5);
console.log(closure(10)); // 16
在上面这个例子中, innerFunction 为闭包,它被定义在 outerFunction 内部。闭包可以访问 outerFunction 中的变量 x 和 innerVar,并且在调用 closure(10) 时可以返回正确的结果 16。
在上面这个例子中, innerFunction 为闭包,它被定义在 outerFunction 内部。闭包可以访问 outerFunction 中的变量 x 和 innerVar,并且在调用 closure(10) 时可以返回正确的结果 16。
闭包可以保存上下文状态,它能记住它被定义时的环境,并在以后使用。由于闭包引用了它外部作用域中的变量,因此闭包可能会导致内存泄露,如果不小心使用。因为闭包会持有它所引用的变量,这些变量不能被垃圾回收器回收。
例如:
代码语言:javascript复制function setupEventListeners() {
var elements = document.querySelectorAll('.clickable');
for (var i = 0; i < elements.length; i ) {
elements[i].addEventListener('click', function() {
console.log(i);
});
}
}
上面的例子中, addEventListener 函数中的匿名函数是闭包,它引用了外部作用域中的 i 变量,当点击元素时,会持有 i 变量的值,如果 setupEventListeners 函数已经被调用并执行完成,那么 i 会变成最后的值,而不是当时的值,这就是一个闭包带来的问题。
总结来说,闭包是 JavaScript 中一个重要的概念,它允许函数访问它被定义时所在的作用域中的变量。闭包可以用来封装私有数据和状态,实现模块化编程。但是也要注意,如果不小心使用闭包可能会导致内存泄露和其他问题。
4、总结
说了这么多,我们明白现代 JavaScript 引擎使用的是标记清除算法去回收垃圾,一般情况下,我们不需要去关心垃圾回收什么时候去进行的。但是我们要注意标记清除有几个弊端,导致有些情况下,垃圾无法被回收。
1、全局变量下挂载的变量无法被回收
2、一个对象被闭包引用,或者被事件监听,它也无法被回收
3、垃圾回收器无法回收循环引用,需要手动解除引用关系释放内存