了解一下V8引擎,怎么回收垃圾

171 阅读4分钟

1. 为什么需要垃圾回收?

当我们在 JavaScript 中创建绑定(变量)或函数时,都会在 JS 引擎的堆区域上为它们分配一些内存。 如果相应的变量不回收,必然会导致内存暴增,从而引发内存泄漏,影响系统性能甚至崩溃。 一旦JS程序执行完毕,这些变量和函数就不再需要保存在内存中了。这时就需要进行垃圾回收。

2. 垃圾回收策略?

2.1 引用计数方法

有对象引用将产生计数字段(用作引用)。每当对象不再有指向它的引用时,它就会被自动收集。

var bar = {
    name: "bar"
};
bar = ""; // name 会进行垃圾回收
bar = null // bar 会进行垃圾回收
缺点:互相引用的对象无法回收(对象相互引用,计数不为0)
var bar = {};
var foo = {};
bar.name = foo;
foo.name = bar; 

2.2 标记清除算法

创建对象时,其 “标记” 位设置为 0(假)。在标记阶段,对于所有可达对象,该位设置为 1(真)。
此操作是使用图遍历深度优先搜索方法执行的。
使用 DFS,我们首先访问根对象,然后进一步访问从根对象可到达的所有对象,依此类推,直到访问所有可到达的对象。
如果存在多个根对象,则将对所有根变量调用标记阶段

object.markedBit = false;

替代文字

  1. 为所有可到达的对象调用标记阶段。
Mark(root){
    if(!root.markedBit){
        root.markedBit = true;
    }
}

forEach(obj in root.next){
    Mark(obj);
}

替代文字

扫描阶段

顾名思义,垃圾收集器通过从堆中清除其内存来“清除 所有无法访问的对象。所有这些对象markedBit = false都从堆内存中清除,只留下那些标记位为 1(true) 的对象。

现在,所有可达对象的标记位值都设置为 false,因为将再次调用整个算法 - 标记阶段,然后是扫描阶段。

扫描阶段算法

forEach(obj in heap){
    if(obj.markedBit){
        obj.markedBit = false;
    }
    else
    heap.release(obj);
}

替代文字

标记和清除算法被称为跟踪垃圾收集器,因为它跟踪程序可以直接或间接访问的整个对象集合

3. V8的垃圾回收策略

它将新生代内存一分为二,每一个部分的空间称为semispace,激活状态的区域为From空间,未激活(inactive new space)的区域为To空间。这两个空间中,始终只有一个处于激活状态,另一个处于未激活状态。我们的程序中声明的对象,变量等首先会被分配到From空间,当进行垃圾回收时,如果From空间中尚有存活对象,则会被复制到To空间进行保存,非存活的对象会被自动回收。当复制完成后,From空间和To空间完成一次角色互换,To空间会变为新的From空间,原来的From空间则变为To空间。这个过程也被称为Scavenger,主要采用了Cheney算法
当 from-space 填满时,将触发次要垃圾回收。如果一个对象在经过多次复制之后依旧存活, To空间使用超过25%, 那么这个对象会被直接晋升到老生代空间中。

3.1 老生代

使用Mark-Sweep-CompactMark-Sweep(标记清除)Mark-Compact(标记整理))算法完成,这种类型的处理保持老年代空间紧凑和干净。

Mark-Sweep(标记清除):那些无法从根对象查询到的对象都将被清除
以下几种情况都可以作为根节点:
1.  全局对象
2.  当前执行函数的局部变量和参数
3.  当前嵌套调用链上的其他函数的变量和参数
Mark-Compact(标记整理): 清扫后,所有存活的对象将被移动到一起。这将减少碎片并提高将内存分配给较新对象的性能。解决Mark-Sweep后内存碎片的问题

v8回收限制

  1. 单线程机制: 程序中的其他各种逻辑都要进入暂停等待阶段,直到垃圾回收结束后才会再次重新执行JS逻辑
  2. 标记或者遍历根节点以及垃圾清理也是一件非常耗时的操作(解决方法: 增量标记,增量整理或者延时整理)

如何避免相关垃圾问题

  1. 手动清除定时器
  2. 少用全局变量以及闭包
  3. 避免重复注册事件
  4. 清除对象的引用