【垃圾回收系列】V8黑科技

304 阅读5分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

上一篇文章里说到了V8在垃圾回收这块有着自己的黑科技,那这个黑科技到底是指什么呢?这篇文章就准备好好聊聊这个话题

xuexi2.jpg

分代式回收

在V8引擎的实现中,它将内存分为新生代和老生代两个区域,那它为什么要这样划分以及是根据什么标准进行划分的呢?

xlsd.png

由于GC本身需要占用时间来执行,如果每一次GC都需要检查所有对象,那么耗费的时间肯定是很多的,那如何减少这个时间呢?V8针对这个问题提出了 分代式垃圾回收 的策略,也就是说将内存中的对象划分为新老两个生代,新生代里都是一些新创建的对象,而老生代里都是经过多次GC后未被回收的对象

所有对象最开始创建都是被放进新生代里的,如果某一个对象经过多次GC而未被回收,那么这个对象就会被放入老生代里。由于新生代里的对象大概率会被回收,所以GC对于新生代的检查频率会比老生代的更高,而老生代里的对象由于已经经过了多次GC而未被回收,所以可以认为老生代里的对象是 常驻内存 的,因此就不需要那么频繁的GC检查,这样就做到按需GC的目的

下面来聊聊两种回收策略的实现过程

新生代

针对新生代区域所采用的回收策略的算法是 Scavenge,这种算法中用到了复制式的算法 Cheney 来实现,那么这个Cheney算法具体是如何设计的呢?

Cheney算法将新生代区域的内存分为 使用区空闲区 两个部分,所有新创建的对象默认都会放入使用区,当使用区被写满时,就要开始进行垃圾回收,这时候就会将所有使用区中的活动对象打上标记,然后将其复制进空闲区,然后将使用区中所有的对象清理掉,最后将使用区和空闲区进行交换,最终就实现了垃圾的清理

当一个活动对象在经历了 多次复制 后仍然存活,那么就可以认为该对象是需要常驻内存的,因此就会将其放入老生代区域进行管理

老生代

针对老生代区域,由于这里的对象都是被认为是需要 常驻内存 的,所以也就不需要像新生代那样频繁地清理,因此V8就是采用标记压缩的方式,定期进行一次GC,最大化利用该区域对象的特性,从而提升了GC的效率

需要注意的是由于通常老生代区域回收垃圾的任务量较大,所以它是结合下述所有方式综合运用来完成GC的

并行回收(Parallel)

通过上文我们了解了分代式回收是如何提升GC效率的,这一章节准备聊聊 并行回收 所带来的的收益

gc.png

我们都是知道js引擎是单线程的,如果js引擎去执行GC,那么脚本中的其他任务都需要等待GC完成后才能被执行,我们称这种行为叫 全停顿(Stop-The-World),设想一下如果GC耗费的时间过长,那么我们的脚本任务就一直没法被执行,从而导致用户感到卡顿

V8引擎针对这个问题提出了 并行回收 的策略,也就是在主线程开启GC时,同时开启多个 辅助线程 进行GC,这样就可以减少在主线程耗费的GC时间,针对新生代区域的垃圾回收就采用了这种方式

增量标记与懒性清理

增量标记避免了全停顿的方式,而是将一次GC标记的过程分为多个小过程,然后穿插在我们的脚本任务之间交替执行,这样就可以在不影响我们脚本执行的情况下完成标记的过程,从而避免因一次GC耗时过长而导致页面卡顿的问题

gc2.png

在增量标记结束后,就要进入清理阶段,而懒性清理的方式具体是指当内存空间足够时,会延迟进入清理过程而优先执行我们的脚本任务,并且在清理时不一定要 一次性 清理干净,而是分为多个小任务,穿插在js脚本任务之间进行,最大程度避免过长占用js主线程的情况

并发回收(Concurrent)

gc3.png

上述的两种方式都会占用到主线程,而 并发回收 是指GC完全交给 辅助线程 执行,js主线程只负责执行我们的脚本任务,这样理论上是最优解,但实际技术实现上相对上述两种会更加复杂,当然这些都是V8团队考虑的事情了,就不需要我们担心了~

结语

到此,关于垃圾回收的相关内容就全部讲解完毕了,希望看完文章的你能和我一样收获满满哦~