Skip to content

垃圾回收机制

在写代码时,每创建一个对象、函数等都需要开辟一块内存,我们并不关注这些,因为引擎已经做好了。 随着程序的运行,会有很多用不到的内存空间,我们称之为垃圾,这些闲置的内存需要及时清理,否则内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。 当然垃圾清理也是由引擎来完成,我们称之为 GCGarbage Collection

垃圾回收策略

在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收

引用计数法(Reference Counting)

为占用物理空间的对象附加一个计数器,每次有其他对象引用计数加一,反之解除引用减一,如果为 0 就将其物理空间收回

优点

  1. 及时,引用数到 0 立刻收回垃圾
  2. 不需要遍历堆区

缺点

  1. 无法解决循环引用的问题:
javascript
function test() {
  let A = new Object();
  let B = new Object();

  A.b = B;
  B.a = A;
}
  1. 需要很多个计数器,浪费空间

标记清除算法(Mark-Sweep)

与计数不同的,该算法的核心变为定期检查是否可达。算法逻辑可以简化成这样

  1. 首先假定所有对象都为垃圾
  2. 从根对象开始遍历,能访问的都改为 “非垃圾”
  3. 清除所有标记为垃圾的空间

缺点

内存碎片化,清理完成后空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。

标记整理(Mark-Compact)算法 可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存

V8 引擎的策略

V8 引擎基于分代(新生代|老生代)式垃圾回收机制,对不同的生代采用不同的策略

新生代

新生代是存活较短的对象(新创建出来的),容量很小。

策略:新生代的内存空间分为使用区和空闲区,新加入的对象会放置到使用区,当使用区满了之后会执行垃圾清理操作,将所有活动的对象进行标记然后拷贝到空闲区,接着把使用区的空间清理掉。最后在进行角色互换,把使用区变成空闲区,空闲区变成使用区。

TIP

  1. 为什么使用空闲的区间?答:空间换时间,相比于移动拷贝的时间复杂度更低。
  2. 当一个对象多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代。

老生代

老生代的对象为存活时间较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大

策略:标记-整理-清除

减少垃圾

  1. 多复用创建出来的对象(Object、Array、Function...),避免重复创建
  2. 清空数组
javascript
// bad 因为会重新创建出一个新的数组
arr = [];

// good
arr.length = 0;
  1. 清除不用的定时器
  2. 减少使用全局变量
  3. 避免使用闭包

参考