垃圾回收机制
在写代码时,每创建一个对象、函数等都需要开辟一块内存,我们并不关注这些,因为引擎已经做好了。 随着程序的运行,会有很多用不到的内存空间,我们称之为垃圾,这些闲置的内存需要及时清理,否则内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。 当然垃圾清理也是由引擎来完成,我们称之为 GC
即 Garbage Collection
垃圾回收策略
在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收
引用计数法(Reference Counting)
为占用物理空间的对象附加一个计数器,每次有其他对象引用计数加一,反之解除引用减一,如果为 0 就将其物理空间收回
优点
- 及时,引用数到 0 立刻收回垃圾
- 不需要遍历堆区
缺点
- 无法解决循环引用的问题:
javascript
function test() {
let A = new Object();
let B = new Object();
A.b = B;
B.a = A;
}
- 需要很多个计数器,浪费空间
标记清除算法(Mark-Sweep)
与计数不同的,该算法的核心变为定期检查是否可达。算法逻辑可以简化成这样
- 首先假定所有对象都为垃圾
- 从根对象开始遍历,能访问的都改为 “非垃圾”
- 清除所有标记为垃圾的空间
缺点
内存碎片化,清理完成后空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
用 标记整理(Mark-Compact)算法 可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
V8 引擎的策略
V8 引擎基于分代(新生代|老生代)式垃圾回收机制,对不同的生代采用不同的策略
新生代
新生代是存活较短的对象(新创建出来的),容量很小。
策略:新生代的内存空间分为使用区和空闲区,新加入的对象会放置到使用区,当使用区满了之后会执行垃圾清理操作,将所有活动的对象进行标记然后拷贝到空闲区,接着把使用区的空间清理掉。最后在进行角色互换,把使用区变成空闲区,空闲区变成使用区。
TIP
- 为什么使用空闲的区间?答:空间换时间,相比于移动拷贝的时间复杂度更低。
- 当一个对象多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代。
老生代
老生代的对象为存活时间较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大
策略:标记-整理-清除
减少垃圾
- 多复用创建出来的对象(
Object、Array、Function...
),避免重复创建 - 清空数组
javascript
// bad 因为会重新创建出一个新的数组
arr = [];
// good
arr.length = 0;
- 清除不用的定时器
- 减少使用全局变量
- 避免使用闭包