18 垃圾回收:V8优化垃圾回收器执行效率
JavaScript 运行在主线程之上,一旦执行垃圾回收算法,需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。把这种行为叫做全停顿(Stop-The-World)。
在执行垃圾回收的过程中,主线程不能做其他事情。如果垃圾回收器占用主线程时间过久,会造成页面的**卡顿 (Jank)(**比如页面正在执行一个 JavaScript 动画,因为垃圾回收器在工作,就会导致这个动画在这 段时间内无法执行,用户体验不佳)。
V8 向现有的垃圾回收器添加并行、并发和增量等垃圾回收技术,来解决垃圾回收效率问题:
- 将一个完整的垃圾回收任务拆分成多个小的任务,避免单个长的垃圾回收任务;
- 将标记对象、移动对象等任务转移到后台线程进行,大大减少主线程暂停的时间,改善页面卡顿的问题,让动画、滚动和用户交互更加流畅。
并行回收
垃圾回收器在主线程上执行的过程中,还会开启多个协助线程,同时执行同样的回收工作:
该方式比较简单,在执行垃圾标记的过程中,主线程不会同时执行 JavaScript 代码,因此 JavaScript 代码也不会改变回收的过程。所以可以假定内存状态是静态的,因此只要确保同时只有一个协助线程在访问对象。
V8 的副垃圾回收器所采用的就是并行策略,它在执行垃圾回收的过程中,启动了多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域。由于数据的地址发生了改变,所以还需要同步更新引用这些对象的指针。
增量回收
并行策略仍然是一种全停顿的垃圾回收方式,在主线程执行回收工作的时候才会开启辅助线程,依然存在效率问题。V8 又引入了增量标记的方式,称之为增量式垃圾回收。
垃圾收集器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。采用增量垃圾回收时,垃圾回收器没有必要一次执行完整的垃圾回收过程,每次执行的只是整个垃圾回收过程中的一小部分工作:
实现增量执行需要满足两点要求:
- 垃圾回收可以被随时暂停和重启,暂停时需要保存当时的扫描结果,等下一波垃圾回收来了之 后,才能继续启动。
- 在暂停期间,被标记好的垃圾数据如果被 JavaScript 代码修改了,垃圾回收器需要能够正确地处理。
没有采用增量算法之前,V8 使用黑色和白色来标记数据。在执行一次完整的垃圾回收之前,垃圾回收器会将所有的数据设置为白色,用来表示这些数据还没有被标记,然后垃圾回收器在会从 GC Roots 出发,将所有能访问到的数据标记为黑色。遍历结束之后,被标记为黑色的数据就是活动数据,那些白色数据就是垃圾数据:
如果内存中的数据只有两种状态,非黑即白,那么当暂停了当前的垃圾回收器之后,再次恢复垃圾回收器,就不知道从哪个位置继续开始执行了:
V8 采用三色标记法,除了黑色和白色,还额外引入了灰色:
- 黑色表示这个节点被 GC Root 引用到了,而且该节点的子节点都已经标记完成了 ;
- 灰色表示这个节点被 GC Root 引用到,但子节点还没被垃圾回收器标记处理,也表明目前正在处理这个节点;
- 白色表示这个节点没有被访问到,如果在本轮遍历结束时还是白色,那么这块数据就会被收回。
垃圾回收器依据当前内存中有没有灰色节点,来判断整个标记是否完成。
标记好的垃圾数据被 JavaScript 修改了,V8 如何处理:
window.a = Object();
window.a.b = Object();
window.a.b.c = Object();
然后又执行了另外 一行代码:
window.a.b = Object(); //d
执行完之后,垃圾回收器又恢复执行了增量标记过程,由于 b 重新指向了 d 对象,所以 b 和 c 对象的连接就断开了:
垃圾回收器不会再次将这个白色节点标记为黑色节点了,因为它已经走过这个路径了。但是这个新的白色节点的确被引用了,还需要想办法将其标记为黑色。
为了解决这个问题,增量垃圾回收器添加了一个约束条件:不能让黑色节点指向白色节点。通常使用写屏障 (Write-barrier) 机制实现这个约束条件(当发生了黑色的节点引用了白色的节点,写屏障机制会强制将被引用的白色节点变成灰色的,这样就保证了黑色节点不能指向白色节点的约束条件)。这个方法保证了垃圾回收器能够正确地回收数据,因为在标记结束时的所有白色对象,对于垃圾回收器来说 ,都是不可到达的,可以安全释放。
并发 (concurrent) 回收
增量垃圾回收都是在主线程上执行的,如果主线程繁忙的时候,增量垃圾回收操作依然会增加主线程处理任务的吞吐量 (throughput)。
并发回收指主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作:
并发回收是这三种技术中最难的一种:
- 当主线程执行 JavaScript 时,堆中的内容随时都有可能发生变化,从而使得辅助线程之前做的工作完全无效;
- 主线程和辅助线程极有可能在同一时间去更改同一个对象,需要额外实现读写锁的一些功能。
在实际使用中,三种技术通常会融合在一起使用,V8 的主垃圾回收器就融合了这三种机制,来实现垃圾回收: