上文我们学习了js引擎垃圾回收机制,这篇文章,我们一起来看看V8引擎垃圾回收机制,看看V8在垃圾回收方面做了哪些优化,有哪些方面性能的提升。v8渲染引擎讲解,可以参考https://mp.weixin.qq.com/s/lOznk5GdDxr9rIRRg4MpaA
V8操作场景及存在限制
在一些实际应用场景中,V8引擎实例的生命周期不会很长,V8这套内存管理机制,在浏览器的应用场景下绰绰有余,如果不幸发生内存泄露等问题,仅仅会影响到某个终端用户,不会对其他用户造成影响。且无论这个V8实例占用了多少内存,最终在关闭页面时内存都会被释放,几乎没有太多管理的必要(当然并不代表一些大型Web应用不需要管理内存)。但在node中,却限制了开发者随心所欲使用内存的想法,一旦内存发生泄漏,久而久之整个服务将会瘫痪(服务器不会频繁的重启)。要知晓V8为何限制内存容量,则需要深入了解V8在内存上使用的策略。只有这样才能够更好的进行内存管理。
在一般的后端开发语言中,在基本的内存使用上没有什么限制。然而node通过js使用内存时就会发现只能使用部分内存:64位操作系统默认使用1.4G,32位操作系统默认使用0.7G。
在了解垃圾回收算法前,我们先来了解一个概念--“全停顿”
垃圾回收算法在执行前,需要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑,这种行为称为 「全停顿」(Stop The World)。例如,如果一次GC需要50ms,应用逻辑就会暂停50ms。为什么会暂停呢?一是因为js是单线程执行的,进入垃圾回收后,js应用逻辑需要暂停,以留出空间给垃圾回收算法运行。另一方面垃圾回收其实是非常耗时间的操作,比如:以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要50ms 以上,做一次非增量式(即将所有GC的数据统一处理,不分区/块概念,一次执行)的垃圾回收甚至要 1s 以上。
V8中的堆数据结构,可分为老生区,新生区,大对象区,map区和代码区,
垃圾回收算法
本文主要针对新生区和老生区来对垃圾回收算法进行解读
1、Scavenge算法
所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,新生区分为:一个Eden区(别名from区)和两个survivor区(别名to区)(比例为8:1:1)
新的对象会首先被分配到 from区,当进行垃圾回收的时候,会先将 from 区中 存活的对象复制到 to区进行保存,对未存活的对象的空间进行回收。
复制完成后, from区和 to区进行调换,to区会变成新的 from区,原来的from区则变成to区。这种算法称之为 ”Scavenge“。
新生代内存回收频率很高,速度也很快,但是空间利用率很低,因为有一半的内存空间处于"闲置"状态。
老生代内存回收
新生代中多次进行回收仍然存活的对象会被转移到空间较大的老生代内存中,这种现象称为晋升。以下两种情况
在垃圾回收过程中,发现某个对象之前被清理过,那么将会晋升到老生代的内存空间中
在 from 空间和 to 空间进行反转的过程中,如果 to 空间中的使用量已经超过了 25% ,那么就将 from 中的对象直接晋升到老生代内存空间中。
老生代占用内存较多(64位为1.4GB,32位为700MB),老生代存活对象多,存活时间久,如果使用Scavenge算法,浪费一半空间不说,复制如此大块的内存消耗时间将会相当长。所以Scavenge算法显然不适合。V8在老生代中的垃圾回收策略采用Mark-Sweep和Mark-Compact相结合
2、Mark-Sweep(标记-清除)算法
老生代采用的是”标记-清除“来回收未存活的对象。
分为标记和清除两个阶段。标记阶段会遍历堆中所有的对象,并对存活的对象进行标记,清除阶段则是对未标记的对象进行清除。
3、标记-整理(Mark-Compact)
标记清除不会对内存一分为二,所以不会浪费空间。但是经过标记清除之后的内存空间会生产很多不连续的碎片空间,这种不连续的碎片空间中,在遇到较大的对象时可能会由于空间不足而导致无法存储。
为了解决内存碎片的问题,需要使用另外一种算法 - 标记-整理(Mark-Compact)。标记整理对待未存活对象不是立即回收,而是将存活对象移动到一边,然后直接清掉端边界以外的内存。
**4、增量标记算法 --- **“全停顿”****
“全停顿” -- 参考上面解释。为了避免垃圾回收时间过长影响其他程序的执行,V8将标记过程分成一个个小的子标记过程,同时让垃圾回收和JavaScript应用逻辑代码交替执行,直到标记阶段完成。我们称这个过程为增量标记算法。
通俗理解,就是把垃圾回收这个大的任务分成一个个小任务,穿插在 JavaScript任务中间执行,这个过程其实跟 React Fiber 的设计思路类似。
5、惰性清理
由于标记完成后,所有的对象都已经被标记,不是死对象就是活对象,堆上多少空间格局已经确定。我们可以不必着急释放那些死对象所占用的空间,而延迟清理过程的执行。垃圾回收器可以根据需要逐一清理死对象所占用的内存页
<article style="margin: 0px 0px 1.5rem; padding: 0px; font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; box-sizing: border-box; line-height: 1.6; color: rgb(33, 37, 41); font-family: -apple-system, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; text-align: left; background-color: rgb(255, 255, 255);">
V8后续还引入了增量式整理(incremental compaction),以及并行标记和并行清理,通过并行利用多核CPU来提升垃圾回收的性能
最后盗图一张(@夏木),来总结V8的垃圾回收机制
老规矩,留一个思考题,下一篇文章给出参考答案。
如何编写V8友好的高性能javascript代码?欢迎小伙伴关注并留言
</article>