Skip to content

⚠️ Important Notice

This post was last updated on: which was . Please pay attention to its timelines.

V8垃圾回收原理

背景介绍

V8是Google的Chrome浏览器中的JS引擎,实现了ECMAScript规范,寄生于浏览器环境中,负责解释执行JS以及执行垃圾回收。

JS中垃圾回收是自动执行的,不需要开发人员手动去执行。

JS中什么时候触发垃圾回收:

  • 块级作用域(如函数)中定义的数据,在块级作用域失效后就会被回收
  • 失活的对象会被回收

NOTE

具体的回收时间是不固定的 不是说失活的对象会被立即回收。毕竟垃圾回收也是在渲染进程的主线程上运行的,和JS代码是互斥的。

js
function test() {
  const a = 123
  return a
}
test()
// test执行完成之后,a会被回收

let obj = new Array()
obj = {}
// new Array()会被回收

常见回收算法

ECMAScript规范并没有明确说明JS引擎应该用什么算法来回收垃圾,不过一般有几种常见的算法。

引用计数

堆中的对象有一个引用计数器,被初始化赋值后计数器就置为1,有其他地方引用后,计数器加1;引用失效后,计数器减1。

js
var obj = new Object() // 计数器1

var otherObj = obj // 计数器加1 为2

otherObj = null // 引用失效 计数器减1,为1

obj = null // 引用失效 计数器减1,为0

当计数器为0时,就会被当做垃圾进行回收。

优点

计算器实现简单,判断效率高,性能也比较好。

缺点

每次更新计数器,增加了时间开销。最大的缺点是无法处理循环引用的问题。

循环引用时,互相引用的变量会导致计数器无法清空,即使对象已经不可访问,会导致内存泄露。解决方法就是使用弱引用,这样不会计数。

根搜索Tracing Collector(可访问性)

GC Roots Set根集

可访问的引用集合。该集合中的引用变量可以访问到对象属性以及调用对象方法。

根搜索算法是以一系列的GC Roots对象为起点,递归遍历找到所有的可访问节点。

如果一个对象从GC Roots找不到时,说明该对象不可访问,是一个垃圾数据。

循环引用时,Roots Set是一个图,其余情况是一个树形结构。

以下对象可以当做根节点:

  • 栈中所有正在运行的引用变量
  • 所有全局对象和全局变量
  • 所有内置对象

大致流程是这样的:内存中对整个堆进行遍历,从GC Roots根对象开始,然后递归遍历所有可到达的对象,可访问的对象标记为存活状态。

NOTE

GC判断对象是否可访问,判断的是强应用,弱引用不会判断。

V8的垃圾回收算法思路

V8采用的是可访问性(reachability)算法,具体如下:

  1. 通过GC Roots标记内存空间中的活动对象非活动对象 简单而言就是从一个根对象出发,遍历所有的对象,将可以遍历到的对象标记为活动对象,对于不可遍历到的对象称为非活动对象。垃圾回收的就是非活动对象

根对象可能是(不仅是):

  • 全局window对象
  • 文档DOM树
  • 存放栈上变量
  1. 回收非活动对象的空间:标记完成之后,统一回收非活动对象的空间。
  2. 内存整理 频繁的垃圾回收之后,可能会造成很多不连续的内存空间,这些不连续的内存空间称为内存碎片。但是有的垃圾回收可能并不会导致内存碎片,所以这一步可选

V8的垃圾回收具体实现

代际假说(The Generational Hypothesis)

代际假说是垃圾回收领域的一个术语,主要有两个特点:

  1. 大部分对象都是"朝生夕死"的。大部分对象在内存中存在的时间都是很短的,随着函数或代码的执行完毕,会很快被销毁。
  2. 不死的对象,会存活的更久。

简单总结一下就是:大部分的对象是会很快被回收的,如果没有被回收大概率会存活很久。

V8的垃圾回收架构

V8根据代际假说,将堆分为两个区域,一个新生代young space,一个老生代old space。新生代中容量较小,一般只有1-8M,存放一些生存时间较短的对象;老生代中容量比较大,存放生存时间较长或者占用空间很大的对象

根据堆的不同,会有两个垃圾回收器,主垃圾回收器以及副垃圾回收器

  1. 主垃圾回收器-Major GC,负责回收老生代中的数据
  2. 副垃圾回收器-Minor GC,负责回收新生代中的数据

副垃圾回收器

新生代中存放的一些生存时间较短的对象,所以会有频繁的垃圾回收

采用Scavenge算法来处理,将新生代对半划分为两个区域,一个区域叫做对象区from-space,一个区域叫做空闲区to-space。等到对象区快要被填满时,就执行一次垃圾回收。 在垃圾回收时,首先对对象区域中的垃圾数据进行标记;标记完成之后,进入垃圾清理阶段,将非垃圾数据复制一份按一定顺序存入空闲区域,这样既完成了清理垃圾也完成了内存整理的工作;然后将对象区域清空掉,再将对象区域和空闲区域调换位置。这种角色互换可以让这两块区域重复使用

新生代区域中执行垃圾回收,会有一次复制操作,会很耗时,这也是为什么新生代空间不能设置太大的原因之一,还有一个原因是,内存小就很容易被占满,垃圾回收的频率会比较高

此外,还有一个对象晋升策略,对于经过了两次垃圾回收的对象,会转入老生代中存储

主垃圾回收器

老生代中存放一些大对象和生存时间较长的对象

主垃圾回收器采用的是标记-清除/标记-整理算法来进行垃圾回收

标记-清除

  1. 标记阶段 从根元素(GC Roots对象)开始,遍历元素,能到达的元素标记为活动对象,不能到达的元素称为非活动对象
  2. 清理阶段 主垃圾回收器会直接清除掉垃圾数据

不过这样会产生大量的内存碎片,所以又引入了一个标记-整理算法来清除内存碎片:标记阶段和标记-清除算法一致,但是清理阶段不会直接清除数据,而是将活动对象全部移向同一端,然后直接清空掉另一端的空间

标记-整理

总结

V8的垃圾回收是基于代际假说的,分为主垃圾回收器和副垃圾回收器两种

副垃圾回收器采用Scavenge算法,分为对象区域空闲区域,等待对象区域满了之后执行垃圾回收,标记垃圾对象,将非垃圾对象复制到空闲区域,然后将两个区域调换达到复用的目的;

主垃圾回收器采用标记-清除/标记-整理算法,标记阶段从根元素开始对元素遍历,将元素分为活动对象和非活动对象,清理阶段直接将非活动对象清除,但是这样会导致大量的内存碎片。所以有了标记-整理算法,不同之处在于清理阶段不会直接清除非活动对象,而是将活动对象全都移到同一侧,将另一侧的空间清除掉,达到了清理和整理两个目的

上一次更新: