zl程序教程

您现在的位置是:首页 >  其他

当前栏目

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

2023-03-14 22:49:37 时间

说明

图解 Google V8 学习笔记



全停顿(Stop-The-World)


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


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


08c0bade3f804b7db98be9f7adccabae.png


怎么解决垃圾回收效率?


   将一个完整的垃圾回收的任务拆分成多个小的任务,这样就消灭了单个长的垃圾回收任务;


   将标记对象、移动对象等任务转移到后台线程进行,这会大大减少主线程暂停的时间,改善页面卡顿的问题,让动画、滚动和用户交互更加流畅。



并行回收


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


48a8af06660b4513aa21f1a51988d0f6.png


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


增量回收


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


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


增量垃圾回收示意图:


656dfb9a789d415a9905b996f1d9c333.png


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


   垃圾回收可以被随时暂停和重启,暂停时需要保存当时的扫描结果,等下一波垃圾回收来了之后,才能继续启动。


   在暂停期间,被标记好的垃圾数据如果被 JavaScript 代码修改了,那么垃圾回收器需要能够正确地处理。


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


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


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


fa094d6631f94bb79008e93123f80e85.png


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


   三色标记法


   黑色:表示这个节点被 GC Root 引用到,且该节点的子节点都已经标记完成;


   灰色:表示这个节点被 GC Root 引用到,但子节点还没被垃圾回收器标记处理,也表明目前正在处理这个节点;


   白色:表示这个节点没有被访问到,如果在本轮遍历结束时还是白色,那么这块数据就会被收回。

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


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


例子:

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


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

e2cd4cf066024427a361bdb99f4bf185.png


然后执行下面的代码

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



垃圾回收器标记示意图:


eb214399775c4e0d923727b78276de78.png


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


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


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



并发回收

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


并发标记的流程示意图:


7e9c01c04e634de881483e21cf8bacd4.png


并发回收难点:


   当主线程执行 JavaScript 时,堆中的内容随时都有可能发生变化,从而使得辅助线程之前做的工作完全无效;


   主线程和辅助线程极有可能在同一时间去更改同一个对象,这就需要额外实现读写锁的一些功能。


总结


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

三种策略示意图:


d224e32f989d46ac8defcc8df31d9cfe.png


  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 等守护进程复活,这个重启只要不是非常频繁往往是业务无感的~