likes
comments
collection
share

一文概述V8垃圾回收机制

作者站长头像
站长
· 阅读数 18

垃圾回收(Garbage Collection)是很多现代编程语言的执行引擎都会有的一种,动态管理应用程序内存的机制,Java的JVM,JavaScript的V8,都有完备的垃圾回收机制。

一、为什么要有垃圾回收机制?

让开发者无需过多在意内存的分配,只需要专注于自己的业务功能开发,虚拟机会自动、智能地管理内存的分配和清理。

二、V8的垃圾回收

首先要清楚,JS的垃圾回收机制是V8引擎提供的,也就是说,Node环境也存在垃圾回收,且和浏览器的机制几乎一样。

V8与JVM一样,也存在垃圾收集器(Garbage Collector)这个概念,只不过我们无法像JVM一样选择和干涉垃圾收集器。

在2016年,V8推出了Orinoco垃圾收集器,并开始增量发布它的一些特性,我们以V8官方的更新日志(v8.dev/blog/orinoc… 为准,简单总结一下它的回收机制。从实用角度来说,了解V8的垃圾回收机制可以让我们更自信地编写js代码,其中的一些设计和算法思想也可以给我们一些启发,从功利角度来看,清楚垃圾回收机制也可以让我们在面对面试官的拷问时也能更加从容应对。

在js中,不再使用的对象被称为垃圾对象,V8需要及时找出并回收这些垃圾对象,才能避免内存泄漏,内存溢出这种致命问题。V8的垃圾回收机制总体上采用分代回收算法和分区算法,分为标记阶段和回收阶段,标记阶段使用可达性分析算法(GCRoots),清除阶段年轻代使用复制算法,老年代使用标记清除和标记整理(压缩)算法。

分代回收和分区算法

什么是分代?

在前人的研究中已经证明,大多数对象都是朝生夕死的,即在分配后几乎立刻变得不可访问,这就是“代际假说 ”,而分代,就是将堆空间分为年轻代(young generation)和老年代(old generation),V8中又将年轻代分为 nursery 和 intermediate,为了最大化提高GC带来的收益,对年轻代进行频繁的GC(Minor GC),对老年代进行必要的GC(Major GC)已经成为几乎所有虚拟机的共识。

什么是分区?

V8 partitions its heap memory into fixed-size chunks, called pages, that are assigned to either young or old generation space.

V8把内存分成若干个固定大小的块,称为page,分配给年轻代或老年代(JVM的G1和ZGC收集器也一样)。这样做可以更好的实现增量回收,也就是V8会维护一个列表,记录每个page的回收价值,然后每次回收只回收一部分回收价值高的page,不过在分区的情况下,也会出现每个区之间的指针引用需要维护的情况,V8使用了remember set来解决这个问题,详情见v8.dev/blog/orinoc…

标记阶段

古老的引用计数法会造成循环引用问题,所以无论是V8还是JVM都没有采用这种算法,他们都使用可达性分析算法来标记活动对象:

标记是找到可访问对象的过程。GC从一组已知的对象指针开始,称为根集。这包括执行堆栈和全局对象。然后,它跟踪每个指向JavaScript对象的指针,并将该对象标记为可访问的。GC跟踪该对象中的每个指针,并递归地继续此过程,直到找到并标记了运行时中可访问的每个对象。

一文概述V8垃圾回收机制 剩余未被标记的对象就是垃圾对象,可以被正确回收。

V8标记阶段本身包括并发标记和并行标记,并非单线程串行进行的,关于STW,写屏障,三色标记,增量标记等,因为不希望篇幅太长,就不在此过多赘述,感兴趣可以去官网自行查看(v8.dev/blog/concur…

回收阶段

清除是一个将垃圾对象留下的内存空间添加到被称为free-list的数据结构中的过程。一旦标记完成,GC就会找到不可访问对象留下的连续间隙,并将它们添加到适当的空闲列表中。free-list按照内存块的大小进行分隔,以便快速查找。将来当我们想要分配内存时,我们只需要查看free-list并找到一个适当大小的内存块。也就是说,垃圾回收并不是把垃圾对象所处的内存清空,而是将这块内存标记为可用状态。

复制算法

V8的年轻代回收使用复制算法(Scavenger)。在一开始,所有的js对象都会分配到年轻代的nursery中,在nursery空间不足时,会触发Minor GC,将活动对象复制到intermediate,intermediate又平分为from区和to区,Minor GC会将nursery中的活动对象和from区的活动对象都复制到to区,然后清空nursery和from区,再将from区和to区身份互换(复制之后有交换,谁空谁是to),如果from区的活动对象是第二次经历GC,并且在这次GC中活了下来,就会被转移到老年代。

一文概述V8垃圾回收机制

一文概述V8垃圾回收机制

一文概述V8垃圾回收机制

标记清除和标记整理

V8的老年代回收阶段使用标记清除和标记整理算法,二者结合使用,并非单独出现

标记清除算法在标记完活动对象后,进行正常清除,也就是上面说的free-list行为,但仅仅是清除的话会带来内存碎片的问题:

一文概述V8垃圾回收机制 (图片非原创,侵删)

由于内存碎片的不连续化,在这些空闲空间分配新的对象时的分配效率会很低,分配过程可能达到(On)复杂度,也可能因为单个碎片空间过小而造成浪费。为了解决这个缺陷,标记-整理算法出现了:

一文概述V8垃圾回收机制 (图片非原创,侵删)

一文概述V8垃圾回收机制 在清除完成后,如果使用的是标记整理算法,则还会将活动对象全部移动到内存一侧,剩余空间则为可分配空间,在进行空间分配时就可以达到(O1)的复杂度,也不会有小空间浪费的情况,那么,代价是什么呢?

整理阶段并非免费的,把活动对象移动到另一侧不仅存在复制对象的消耗,因为活动对象的内存地址已经改变,还要考虑移动后维护活动对象的指针,所以在上面V8官方的描述中,只有在必要时(Full GC)才会进行内存整理,且由专门的线程去并发执行。

三、垃圾回收中的一些概念

并行(Parallel)

并行是指主线程和辅助线程执行大致相同的工作

一文概述V8垃圾回收机制

并发(Concurrent)

并发是指主线程不断执行js,辅助线程在后台执行GC

一文概述V8垃圾回收机制

增量回收(Incremental)

增量是指主线程间歇性地执行GC

一文概述V8垃圾回收机制

STW(stop the world)

在GC时停掉JS主线程,因为在回收时对象引用可能发生改变,带来一系列问题,所有垃圾回收器和算法都不可能完全避免STW,只能尽量减少STW的时间,比如采用并发回收

结语

垃圾回收是在暂停时间与吞吐量之间权衡的艺术,本文也只是简单概述了一下V8的回收机制,至于它和浏览器的渲染线程,与DOM之间的句柄联系,还有诸多优化手段都没有提及,感兴趣的话可以自行到V8官网查看,如果本文有描述错误,欢迎各位指出,感谢。