V8内存浅析

简单总结一下V8的内存管理机制,虽然前端工程师的大部分工作都不需要手动管理内存,但是了解JavaScript引擎的内存管理和垃圾回收能帮助我们更好的理解我们代码的运行环境,避免出现内存泄漏等问题,提高代码质量

V8内存限制

首先,我们知道浏览器中一个tab页独享一个javascript引擎实例,也就是V8实例,而V8对内存的使用是有大小限制的,64位系统中约为1.4G,32位系统约为0.7G,也就是说这是一个V8引擎实例所能操作的内存空间大小上限,当然,这对于一个浏览器tab页来说是绰绰有余的。

为何要做内存大小限制

这与V8的垃圾回收机制有关,JavaScript中的对象是存在堆内存中的,按官方的说法,以1.5GB的垃圾回收堆内存来说,V8做一次小的内存回收(Scavenge后面会说)需要50毫秒以上,做一次非增量式的回收需要1秒以上。

JavaScript引擎是单线程的,GC程序穿插在代码执行过程中,并且是互斥的,也就是说在垃圾回收过程中,JavaScript代码暂停执行,业务代码需要等待,GC占用的时间越长,程序执行效率越低,一次GC占用太长时间,甚至会使得程序得不到响应,影响用户体验。因此引擎的优化历史中总是包含GC的效率提升。

不管垃圾回收的算法是引用计数还是标记清除,GC(自动垃圾清理程序)都是要遍历内存才知道哪些对象是需要被回收的,因此内存占用越大,垃圾回收效率必然越低,V8为避免应用的性能和响应能力急剧下降,限制了堆内存的大小。

V8内存分带

V8的垃圾回收策略主要基于分代式垃圾回收机制。

因此V8将内存区分为新生代内存区和老生代内存区(本文仅简单介绍V8内存管理,因此不提及大对象区、代码区、cell和map区等),新生代内存区非常小,一般只有几十M,主要存放的都是存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

新生代内存区会经历较频繁的垃圾回收,而老生代中的垃圾回收则相对不那么频繁。通过分代机制,在保证内存使用状况良好的情况下,减少了每次GC的损耗,这也是V8高性能的一环。

Scavenge算法

在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收。

具体过程如下:它将新生代内存区等分为两部分,分别为使用中的活跃区和闲置中的非活跃区,当我们新创建一个对象,就会在新生代活跃区申请一段内存空间,并将对象存入内存,当开始进行垃圾回收时,会检查所有活跃区中的对象,其中存活的对象中如果之前没有经历过Scavenge回收并且非活跃区空间使用低于25%,会被复制到非活跃区,否则直接被复制到老生代中,而非存活对象占用的内存空间将会被释放掉。之后活跃区与非活跃区角色互换。

由于新生代中对象存活时间较短,每次垃圾回收存活的对象都很少,因此复制的成本很低,使用Scavenge算法虽然浪费掉一半的内存空间,但是GC的效率得到很大提高(有点空间换时间的意思),非常适合新生代频繁的垃圾回收。

Mark-Sweep & Mark-Compact

在老生代中,存活对象占大部分,并且老生代空间较大,就不适合使用Scavenge了,使用的是一种称为标记清除和标记整理的算法。V8在老生代中采用Mark-Sweep和Mark-Compact结合的方式进行垃圾回收。

具体过程如下:Mark-Sweep是一个标记清除的过程,首先遍历老生代中所有对象,标记活着的对象,然后直接清除没有被标记的对象,释放这部分内存空间,由于老生代中死亡对象只占较小一部分,因此能够高效处理。但是Mark-Sweep存在一个问题,在一次标记清除后,内存空间会出现不连续状态,造成大量内存碎片,后续的分配过程中,内存碎片可能不足以存放要分配的对象,就会提前触发不必要的垃圾回收;因此就有了Mark-Compact,Mark-Compact是标记整理的意思,标记过程与Mark-Sweep一致,整理的过程中,将活着的对象往内存一端移动,移动完直接清理掉边界外的内存即可。

Mark-Compact避免了内存碎片的问题,但做了更多的事–移动,明显效率要低于Mark-Sweep,并且我们其实并不需要每次回收都进行内存整理,因此V8主要使用Mark-Sweep,在内存空间不足以对从新生代中复制来的对象进行分配时才使用Mark-Compact。

增量标记

老生代内存区相对很大,存放的对象可能也很多,如果一次GC要标记所有对象,并且进行清理的话,可能会造成线程较长时间的停顿,因此一种增量标记的技术被引入引擎的垃圾回收过程,思想就是将一次标记过程拆分为多段,与应用逻辑交替执行直到标记完成。

V8经过增量标记优化后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。

总结

因此,个人认为,V8在做内存管理的过程中,无论是限制内存大小还是采用分代式垃圾回收,目的都是为了保证在内存使用良好的状态下减少每次GC的损耗(线程停顿时间),从而提升引擎的性能。

参考:本文参考朴灵大神《深入浅出nodejs》第5章内存控制


如有错漏,欢迎指正~