V8引擎对垃圾回收机制的优化

分代式垃圾回收

对内存中的对象进行分代,将大、老、存活时间较久的对象归为老生代,将小、新、存活时间较短的对象归为新生代,新生代与老生代采取不同的垃圾回收频率和垃圾回收策略

新老生代

新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量,而老生代的对象为存活事件较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大

新生代垃圾回收

新生代对象是通过一个名为 Scavenge 的算法进行垃圾回收,在 Scavenge算法 的具体实现中,主要采用了一种复制式的方法即 Cheney算法Cheney算法 中将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区,一个是处于闲置状态的空间我们称之为 空闲区

两个阶段

  • 标记阶段

    对使用区所有活动对象标记,标记后复制一份到空闲区并排序(防止内存碎片产生)

  • 清理阶段

    清空使用区所有对象,清理空闲区非活动对象,将空闲区和使用区角色互换,由此循环

当一个对象经过多次复制后仍然存活,那么它将会被认为是生命周期较长的对象,随后便后晋升,被移动到老生代中,采用老生代的策略进行管理

另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge 回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配

老生代垃圾回收

老生代的垃圾回收采用简单的标记清除法,并采用了标记整理算法来优化内存空间避免生成内存碎片

标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动

优化策略

  • 并行回收
  • 增量标记与惰性清理
  • 并发回收

1.并行回收(Parallel)

在介绍并行之前,先要了解一个概念 全停顿(Stop-The-World)JavaScript 是一门单线程的语言,它是运行在主线程上的,那在进行垃圾回收时就会阻塞 JavaScript 脚本的执行,需等待垃圾回收完毕后再恢复脚本执行,这种行为即为 全停顿

比如一次 GC 需要 60ms ,那应用逻辑就得暂停 60ms ,假如一次 GC 的时间过长,对用户来说就可能造成页面卡顿等问题

并行回收引入多个辅助线程同时进行处理,这样可以有效缩短垃圾回收过程所消耗的时间,但垃圾回收仍然会占据主线程时间,因此在垃圾回收过程中,内存中的对象是静态的(不执行js脚本对象的引用关系就不会发生变化

新生代对象一般采用并行策略,在执行垃圾回收的过程中,会启动了多个线程来负责新生代中的垃圾清理操作。

2.增量标记与懒性清理

我们上面所说的并行回收策略虽然可以增加垃圾回收的效率,对于新生代垃圾回收器能够有很好的优化,但是其实它还是一种全停顿式的垃圾回收方式,对于老生代来说,它的内部存放的都是一些比较大的对象,对于这些大的对象 GC 时哪怕我们使用并行策略依然可能会消耗大量时间

增量就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿(间断性执行),这样交替多次后完成一轮 GC 标记,这样可以有效避免主线程的堵塞,不过又会出现两个问题,一个是每一小次 GC 标记执行完之后去执行任务程序,而后如何恢复,其次执行任务程序时内存中标记好的对象引用关系被修改了怎么办

三色标记法

三色标记法即使用每个对象的两个标记位和一个标记工作表来实现标记,两个标记位编码三种颜色:白、灰、黑

  • 白色指的是未被标记的对象(最终被回收)
  • 灰色指自身被标记,成员变量(该对象的引用对象)未被标记
  • 黑色指自身和成员变量皆被标记

采用三色标记法后我们在恢复执行时,可以直接通过当前内存中有没有灰色节点来判断整个标记是否完成,如没有灰色节点,直接进入清理阶段,如还有灰色标记,恢复时直接从灰色的节点开始继续执行就可以。

写屏障

如果在执行js阶段,存在对象的引用关系被修改的情况,那么一般有两种,一种是已经被标记为黑色或者灰色的对象不再被引用了,另外一种是引用了新的对象。前者影响不大,在下一次垃圾回收过程中仍然会被清理,但后者由于是新引用的对象,所以该对象会被标记为白色,这样该对象就有可能会在次轮的回收阶段被清理。

为了解决这个问题,V8 增量回收使用 写屏障 (Write-barrier) 机制,即一旦有黑色对象引用白色对象,该机制会强制将引用的白色对象改为灰色,从而保证下一次增量 GC 标记阶段可以正确标记。

懒性清理

增量标记其实只是对活动对象和非活动对象进行标记,对于真正的清理释放内存 V8 采用的是惰性清理(Lazy Sweeping)

增量标记完成后,惰性清理就开始了。当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理过程稍微延迟一下,让 JavaScript 脚本代码先执行,也无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕,后面再接着执行增量标记。

增量标记与懒性清理的优缺点

优点:使得主线程的停顿时间大大减少了,让用户与浏览器交互的过程变得更加流畅

缺点:并没有减少主线程的总暂停的时间,甚至会略微增加

3.并发回收(Concurrent)

主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起。但它需要考虑主线程在执行 JavaScript 时,堆中的对象引用关系随时都有可能发生变化。

新生代垃圾回收采用并行策略来提高回收效率

老生代垃圾回收会混合使用这三种优化策略,在标记阶段以并发的形式标记(不影响主线程),在清理阶段采用并行的形式,此外,在清理阶段还会采取增量的方式分批穿插在js任务之间执行清理任务

总结

本文主要总结了V8引擎优化后的垃圾回收机制,除了新老生代自己的回收方式外,还有三种优化策略。如有错误,请加以指正,万分感激。

猜你喜欢

转载自blog.csdn.net/m0_64023259/article/details/124018665