V8 垃圾回收

什么是垃圾回收

js 中基本类型是储存在栈空间里的,复杂类型是储存在堆空间里,同时栈空间里保存这对它的引用。这是数据的储存方式,如果这些数据被使用过后就不再被需要了,就成了我们常说的垃圾数据,就需要被清理掉。

当调用栈的函数执行完毕,会被弹出,同时销毁函数的执行上下文,就会导致内存里没用的数据成为垃圾数据,会被js 里的垃圾回收器进行自动回收。

代际假说

代际假说有两个特点:

  • 第一个大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经内存分配。就很快变得不可访问
  • 第二个是不死的对象,会活的更久

根据上述两个特性,v8 将堆内存分为两个区域,新生代与老生代:

  • 新生代:存放生存时间较短的对象,通常支持1~8M大小的容量
  • 老生代:存放生存时间较长的对象,比较大

根据上述两个区域,垃圾回收器也就分成里两种,主、副垃圾回收器:

  • 副垃圾回收器:主要负责新生代垃圾的回收
  • 主垃圾回收器:主要负责老生代的垃圾回收

垃圾回收的原理

执行流程

  1. 第一步是标记空间中活动对象何非活动对象。即还在使用的对象,可以进行垃圾回收的对象,即标记垃圾数据。
  2. 回收非活动对象所占据的内存。也就是垃圾清理阶段。
  3. 内存整理。垃圾回收后,内存里会出现大量不连续的小块内存,即内存碎片。不利于内存的利用,当需要大量连续内存比如数组时,就会导致内存不足。故需要在最后一步,对这些内存进行整理。

副垃圾回收器

主要负责新生代的垃圾回收,通常情况大部分小的对象都会被分配到新生区,会经常发生垃圾回收的区域。

新生代采用 Scavenge 算法进行处理。将新生代分为两个区域,一半为对象区域,一半为空闲区域。

/posts/v8-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/v8%E5%A0%86%E5%8F%A0%E7%A9%BA%E9%97%B4.png

新加入的对象都会被放在对象区域,等对象区域快满的时候,就会执行一次垃圾清理操作。

  • 首先会对对象区域中的垃圾进行标记,标记完成会进入清理垃圾阶段
  • 副垃圾回收器会将未标记的存活对象复制到空闲区域,同时还会对这些对象进行有序的排列,所以复制的过程同时完成里内存整理,复制后空闲区域就没有内存碎片
  • 复制完成,对象区域与空闲区域角色翻转。原来的对象区域变成空闲区域,空闲区域变成对象区域,清空空闲区域,就这样完成垃圾回收。

由于会进行对象的复制操作,所以这块新生代内存不会太大,有错也会造成对象区域很容易被填满。为里解决这种问题, v8 采取里对晋升策略,经过两次垃圾回收依然存活的对象会被移动到老生区。

主垃圾回收器

主垃圾回收器主要负责老生生区的垃圾回收。除老新生区中晋生的对象,一些大的对象会直接被分配到老生区。因此老生区中有两个特点:

  • 对象占用空间大
  • 对象存活时间长

由于老生区对象比较大,就不能采用新生区的 Scavenge 算法进行垃圾回收。主垃圾回收器采用的是 标记 - 清除 的算法进行垃圾回收。

  • 标记阶段:从一组根元素开始,递归遍历这组元素,在这个遍历的过程中,能达到的元素称为活动对象,没有达到的元素可以判断为垃圾对象
  • 垃圾清除:对老生区中标记为垃圾的对象进行清理,在清理前首先会让活动对象向一段移动,然后直接清理掉端边界以外的内存。

全停顿

由于 js 是单线程的,并且与渲染线程同属一个,所以一旦执行垃圾回收,会暂停 js 脚本,待垃圾回收完毕再回复脚本执行。我们吧这种行为称之为全停顿。

在新生代中,由于空间较小,全停顿影响不大,但是老生代空间对象很大,就不能使用全停顿了。v8 为了减少这种停顿,将标记阶段分为一个个子标记阶段,标记与js逻辑交替执行,直至标记阶段完成。然后进行清理+整理的过程。