图解 Google V8 # 21 :垃圾回收:V8是如何优化垃圾回收器执行效率的?

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解 Google V8 # 21 :垃圾回收:V8是如何优化垃圾回收器执行效率的?相关的知识,希望对你有一定的参考价值。

说明

图解 Google V8 学习笔记

全停顿(Stop-The-World)

由于 javascript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿(Stop-The-World)

全停顿的执行效果示意图:下面的 200 毫秒内无法执行其他事情,可能造成页面的卡顿 (Jank)。

怎么解决垃圾回收效率?

  1. 将一个完整的垃圾回收的任务拆分成多个小的任务,这样就消灭了单个长的垃圾回收任务;
  2. 将标记对象、移动对象等任务转移到后台线程进行,这会大大减少主线程暂停的时间,改善页面卡顿的问题,让动画、滚动和用户交互更加流畅。

并行回收

并行回收机制:主线程在执行垃圾回收的任务时,引入多个辅助线程来并行处理,这样就会加速垃圾回收的执行速度。

V8 的副垃圾回收器所采用的就是并行策略,它在执行垃圾回收的过程中,启动了多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域。由于数据的地址发生了改变,所以还需要同步更新引用这些对象的指针。

增量回收

老生代存放的都是一些大的对象,如 window、DOM 这种,完整执行老生代的垃圾回收,时间依然会很久。这些大的对象都是主垃圾回收器的,所以在 2011 年,V8 又引入了增量标记的方式,称之为增量式垃圾回收。

所谓增量式垃圾回收,是指垃圾收集器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。

增量垃圾回收示意图:

实现增量执行,需要满足:

  1. 垃圾回收可以被随时暂停和重启,暂停时需要保存当时的扫描结果,等下一波垃圾回收来了之后,才能继续启动。
  2. 在暂停期间,被标记好的垃圾数据如果被 JavaScript 代码修改了,那么垃圾回收器需要能够正确地处理。

V8 是如何实现垃圾回收器的暂停和恢复执行的?

  1. 黑白色标记数据(采用增量算法之前)

在执行一次完整的垃圾回收之前,垃圾回收器会将所有的数据设置为白色,用来表示这些数据还没有被标记,然后垃圾回收器在会从 GC Roots 出发,将所有能访问到的数据标记为黑色。遍历结束之后,被标记为黑色的数据就是活动数据,那些白色数据就是垃圾数据。

这种标记存在的问题:当你暂停了当前的垃圾回收器之后,再次恢复垃圾回收器,那么垃圾回收器就不知道从哪个位置继续开始执行。

  1. 三色标记法
  • 黑色:表示这个节点被 GC Root 引用到,且该节点的子节点都已经标记完成;
  • 灰色:表示这个节点被 GC Root 引用到,但子节点还没被垃圾回收器标记处理,也表明目前正在处理这个节点;
  • 白色:表示这个节点没有被访问到,如果在本轮遍历结束时还是白色,那么这块数据就会被收回。

引入灰色标记之后,垃圾回收器就可以依据当前内存中有没有灰色节点,来判断整个标记是否完成,如果没有灰色节点了,就可以进行清理工作了。如果还有灰色标记,当下次恢复垃圾回收器时,便从灰色的节点开始继续执行。

V8 是如何处理被 JavaScript 修改标记好的垃圾数据?

例子:

window.a = Object()
window.a.b = Object()
window.a.b.c = Object() 

当执行到这里,垃圾回收器标记示意图:

然后执行下面的代码

window.a.b = Object() // d

垃圾回收器标记示意图:

当垃圾回收器将某个节点标记成了黑色,然后这个黑色的节点被续上了一个白色节点,那么垃圾回收器不会再次将这个白色节点标记为黑色节点了,因为它已经走过这个路径了。

解决方案:添加约束条件——不能让黑色节点指向白色节点。

通常使用写屏障 (Write-barrier) 机制实现这个约束条件:当发生了黑色的节点引用了白色的节点,写屏障机制会强制将被引用的白色节点变成灰色的,这样就保证了黑色节点不能指向白色节点的约束条件。这个方法也被称为强三色不变性

并发回收

所谓并发 (concurrent) 回收,是指主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作。

并发标记的流程示意图:

并发回收难点:

  1. 当主线程执行 JavaScript 时,堆中的内容随时都有可能发生变化,从而使得辅助线程之前做的工作完全无效;
  2. 主线程和辅助线程极有可能在同一时间去更改同一个对象,这就需要额外实现读写锁的一些功能。

总结

V8 的主垃圾回收器就融合了这三种机制,来实现垃圾回收。

三种策略示意图:

  1. 在主线程执行 JavaScript,辅助线程就开始执行标记操作
  2. 标记完成之后,主线程在执行清理操作时,多个辅助线程也在执行清理操作。
  3. 清理的任务会穿插在各种 JavaScript 任务之间执行。

内存泄漏问题的定位

来自 sugar 网友:

内存泄漏问题的定位,一般是通过 chrome 的 devtool 中 memory report 来观察的,nodejs 环境中的 mem leak case 我们研究的比较多,一般通过结合 memwatch 等 c++ 扩展包把 report 文件 dump 在线上机磁盘上,然后 download 下来在本地的 chrome 浏览器 devtool 中进行复盘。比较常见的 case 是一些 js 工程师对 scope 的理解不够深,复杂的闭包里出现了隐式的引用持有却没释放。此类问题一般隐蔽性比较强,而且如果不是大厂的业务线(业务高峰产生高并发环境),往往可能压根发现不了,因为就算有 leak 内存逐渐增长到 v8 的 heap limit 后 node 进程死掉就会被 pm2/forever 等守护进程复活,这个重启只要不是非常频繁往往是业务无感的~

以上是关于图解 Google V8 # 21 :垃圾回收:V8是如何优化垃圾回收器执行效率的?的主要内容,如果未能解决你的问题,请参考以下文章

图解 Google V8 # 22 :关于内存泄漏内存膨胀频繁垃圾回收的解决策略(完结篇)

图解 Google V8学习笔记合集 23 篇(完结)

V8 JavaScript引擎研究垃圾回收器的实现

深入理解V8的垃圾回收原理

「译」Orinoco: V8的垃圾回收器

V8 堆栈空间和垃圾回收机制