v8垃圾回收

Source

内存的生命周期

内存的生命周期可以分为三个阶段:

  • 内存分配:按需分配内存
  • 内存食用:读写已经分配的内存
  • 内存释放:释放不再需要的内存
    在js当中内存的分配是根据变量的数据类型进行分配的:
  • 栈内存:是一种简单储存,适合存放生命周期比较短,占用空间较少而且固定的数据,由系统直接管理,进行内存的分配和自动释放。一般都是基本数据类型分配在栈内存当中。(基本数据类型有Number;Boolen;String;Null;Undefined;Symbol)
  • 堆内存:是很大的一个存储空间。堆内存按需进行内存空间的申请,动态分配且不连续,值大小不固定,访问速度比栈内存要慢,无用数据需要JS引擎程序主动去回收。一般都是引用数据类型的值放在里面

注意:引用类型的数据会同时分配在栈内存和堆内存,其地址存在栈内存,其具体内容存在堆内存中(这个也是基本数据类型和引用数据类型的区别)

特殊:全局变量(使用var声明)和闭包都是存储在堆内存当中

v8垃圾回收算法

在js当中,根据对象的存活的周期分为两种类型:

  • 生存时间较短:对象经过一次垃圾回收之后不在被使用,就被释放回收
  • 生存时间较长:对象经过多次垃圾回收之后还是继续存活。

对于不同的生存时间,v8使用分代回收的方法来处理。v8将堆划分为两个部分:新生代和老生代

区域 垃圾回收器
新生代 副垃圾回收器
老生代 主垃圾回收器

新生代

副垃圾回收器主要使用Scavenge算法进行垃圾回收,Scavenge算法是一种典型的使用空间换时间的算法,它将新生代空间的堆内存分为2块同样大小的空间,称为Semispace,我们将处于使用状态的区域叫作From空间,闲置的区域叫作To空间

Scavenge

Scavenge算法在每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。
Scavenge算法工作方式是将From空间中存活的活动对象复制到To空间中,并将这些对象的内存有序的排列起来,然后将From空间中的非活动对象的内存进行释放,完成之后,将From空间和To空间进行互换,这样可以使得新生代中的这两块区域可以重复利用。

图例

  1. 标记活动对象和非活动对象(红色肥活动,绿色活动)

在这里插入图片描述

  1. 复制From空间的激活对象到To空间并进行排序

在这里插入图片描述

  1. 清除From空间中未激活的对象
    在这里插入图片描述
  2. 互换From和To空间
    在这里插入图片描述
  3. 循环以上步骤

注意:在新生代中,还进行了进一步的细分,分为nurdery 子代和intermediate子代连哥哥区域,一个对象第一次分配内存的时候会被分到nurdery中,如果进行下次垃圾回收的时候这个对象还存在于新生代当中,这时候会将它移动到intermediate中,再进行下一次垃圾回收,如果这个对象还在新生代中,副垃圾回收器会将该对象移动到老生代中,这个移动的过程被称为晋升

老生代

老生代由于存活时间比较 长,如果按照新生代的scavenge算法进行回收时,From与To之间循环会造成严重的空间资源浪费,效率低下,所以,针对老生代,主垃圾回收器采用了Mark-Sweep(垃圾扫除)算法和Mark-Compact(垃圾整理)算法进行垃圾回收

Mark-Sweep

首先是标记阶段,将堆中的所有对象进行递归(包括调用栈),在遍历的过程,标记激活对象和未激活对象,标记完成后就会进行清除。清除是将不可访问的对象留下的内存空间添加到空闲链表。目的是将来如果要给新对象分配内存时,就可以直接从空闲链表中进行分配。

Mark-Compact

由于Mark-Sweep算法,产生了许许多多的内存碎片。如果内存碎片过多的画会导致大的激活对象没有办法分配到相应的连续内存,所以,为了减少并整理内存碎片,就需要Mark-Compact来进行整理。
首先就是标记过程,将堆中的所有对象进行递归(包括调用栈),在遍历的过程,标记激活对象和未激活对象,然后将所有激活对象移到一端,然后清理边界以外的内存,从而让激活对象占用连续的内存。

图例

  1. 标记激活对象和未激活对象
    在这里插入图片描述

  2. 清除未激活对象
    在这里插入图片描述

  3. 将激活对象移动到一端
    在这里插入图片描述

  4. 清理边界以外的内存

v8垃圾回收的弊端

js是运行在主线程之上的,为了避免js的应用逻辑和垃圾回收产生冲突,垃圾回收正在执行的时候会占用js引擎,正在执行的js脚本就会暂停,等到垃圾回收结束后js脚本才会继续执行,这个过程称为全停顿
在v8的分代式垃圾回收中,新生代默认的内存较小,所以大部分都是分到老生代,由此垃圾的标记,清理造成的停顿就会比较严重。

v8垃圾回收优化

为了优化停顿带来的影响,v8中加入了其他的垃圾回收技术:

  • 标记阶段

增量标记:在老生代中,存活对象较多,较大,全停顿造成的页面卡顿或者空白较多较长,产生的影响比较大,为了减少全停顿的时间,v8对标记进行了优化,当垃圾达到一定的数量的时候,将一次停顿进行的标记过程分成了各个小步。每执行一小步就运行一点js逻辑代码,然后交替完成标记,减少每次停顿的时间。(有点像是减少时间,增加次数)
在这里插入图片描述

  • 清理阶段

延迟清除:在增量标记之后就要对非激活对象进行清除。但是其实就算这个阶段不清理,垃圾回收器剩余的内存也能让js逻辑代码跑起来,所以就有了来进行延迟清理或者之清理部分垃圾的算法—延迟清除。
写屏障
虽然在标记阶段增加了增量标记改善了全停顿的问题,但是也引发了一个新的问题:标记和代码之间的穿插可能会让对象的引用和标记发生错误。应用程序必须通知垃圾收集器关于改变对象的所有操作。由此v8采用Dijkstra风格的写屏障来实现通知。例如object.field=value的写操作之后,V8会插入写屏障代码。写屏障机制强制不变黑的对象指向白色对象。这也被称为强三色不变性,保证应用程序不能在垃圾收集器中隐藏活动对象,因此标记结束时的所有白色对象对于应用程序来说都是不可达的,可以安全释放。
并行
并行是指主线程和辅助线程同时执行大致相等的工作量,这也是解决全停顿的方法。与延迟清除不一样的是总的停顿时间是因为辅助线程的参与而得到减少,由于辅助线程没有运行js,所以每个辅助线程只需要确保同步GC就可以。
在这里插入图片描述
参考文献