V8 垃圾回收机制

垃圾回收分为两个阶段:

  1. 标记阶段:判断一个对象是否可以回收,一般使用引用计数法可达性分析法
  2. 回收阶段:销毁可以被回收的对象,常见算法有:标记清除算法复制算法标记压缩算法分代算法

常用 GC 算法 #

标记清除算法 #

标记清除法是现在GC算法的基础。标记清除算法的执行过程分为两个阶段:标记阶段、清除阶段。

  • 标记阶段会通过可达性分析将不可达的对象标记出来。
  • 清除阶段会将标记阶段标记的垃圾对象清除。 标记清除法的缺陷是回收后会产生大量不连续的内存空间,即内存碎片

复制算法 #

复制算法会将内存空间分为两块,每次只使用其中一块内存。 复制算法同样使用可达性分析法标记除垃圾对象,当GC执行时,会将非垃圾对象复制到另一块内存空间中,并且保证内存上的连续性,然后直接清空之前使用的内存空间。然后如此往复。 该算法在存活对象少,垃圾对象多的情况下,非常高效。其好处是不会产生内存碎片,但坏处也是显而易见的,就是直接损失了一半的可用内存

标记压缩算法 #

标记压缩算法可以解决标记清除算法的内存碎片问题。 其算法可以看作三步:

  1. 标记垃圾对象
  2. 清除垃圾对象
  3. 内存碎片整理 标记压缩算法的碎片整理会造成较大的消耗

分代算法 #

首先,标记清除算法、复制算法、标记压缩算法都有各自的缺点,如果单独用其中某一算法来做GC,会有很大的问题。例如,标记清除算法会产生大量的内存碎片,复制算法会损失一半的内存,标记压缩算法的碎片整理会造成较大的消耗。 其次,复制算法和标记压缩算法都有各自适合的使用场景。复制算法适用于每次回收时,存活对象少的场景,这样就会减少复制量。标记压缩算法适用于回收时,存活对象多的场景,这样就会减少内存碎片的产生,碎片整理的代价就会小很多。 分代算法将内存区域分为两部分:新生代和老年代。 根据新生代和老年代中对象的不同特点,使用不同的GC算法。

  • 新生代对象的特点是:创建出来没多久就可以被回收(例如虚拟机栈中创建的对象,方法出栈就会销毁)。也就是说,每次回收时,大部分是垃圾对象,所以新生代适用于复制算法。
  • 老生代的特点是:经过多次GC,依然存活。也就是说,每次GC时,大部分是存活对象,所以老年代适用于标记压缩算法。

V8 垃圾回收 #

V8 的垃圾回收策略主要基于分代式垃圾回收机制。 V8对内存大小做了限制,64位操作系统最大为1.4GB,32为系统最大为0.7GB。这么做的原因主要有以下两点:

  1. V8主要是服务于浏览器的,浏览器不太可能遇到需要大内存的场景。
  2. 64位系统之所以设计成1.4GB的上限是有考量的,因为如果内存超过1.4GB的时候,V8做一次最小的垃圾回收需要50ms以上,做非增量垃圾回收需要1秒以上,在回收过程中会阻塞JS执行,这个时候你会觉得浏览器卡死了。所以设置1.4GB最大内存是一个平衡的选择。 因为垃圾回收会阻塞JS代码,所以怎么高效的进行垃圾回收就显得非常重要了。V8 的垃圾回收策略主要基于分代式垃圾回收机制

V8如何回收新生代 #

新生代因为存放的是一些存活时间短的对象,所以为了效率考虑,其回收频率也会比较高。其回收策略采用的是标记 + 复制。

v8-from-to

如上图所示,新生代内存等分为两个区域,From为活动区域,To为复制区域。 当我们分配对象时,先是在From空间中进行分配,当要触发垃圾回收的时候,From就会先进行标记,标记完成后,将可达对象(活动对象)复制到To区域,然后把From区域整理进行清除,清除完成后From和To区域交换一下。

晋升 #

我们前面说过新生代内存上限是32MB,这点内存很容易就能达到,所以如果一些对象经常存在新生代里的话,很容易新生代内存就占满了,所以V8存在一种晋升机制,但满足下面条件的时候,新生代对象就会移动到老生代区域,简称晋升,这个过程发生在From区域将活动对象拷贝至To区域过程中。

  1. 一轮垃圾回收(GC)后还存活的新生代对象需要晋升。也就是说上一轮垃圾回收后,这一轮垃圾回收发现你还是活动对象,就需要把你晋升到老生代区域。
  2. To空间的使用率超过25%,如果超过25%,后续对象直接晋升。这也是个衡量结果,超过25%,后续新进来的对象就会有些不够用了。

V8如何回收老生代 #

老生代存一些持续时间久的对象,比如全局变量,闭包等。它可分配内存大,而且回收频率不像老生代那么频繁。所以它的垃圾回收机制也和不太一样。 老生代主要是用标记清除策略进行垃圾回收,但是前面说过,标记清除会产生碎片化内存,当新生代对象要晋升到老生代,而老生代没有空间可存的时候,老生代就会进行一次标记整理进行空间优化,优化后在进行晋升操作。

增量标记 #

垃圾回收(分标记和回收两个阶段)会阻塞JS执行,全堆垃圾回收带来的停顿时间会较长。 V8 从标记阶段入手,将原本需要一口气停顿完成的动作改成增量标记,即程序执行和垃圾回收穿插着来,这样用户的感知就不明显了。图示如下:

v8-2

V8 在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的 1/6 左右。 V8 后续还引入了延迟清理(lazy sweeping)增量式整理(incremental compaction),让清理和整理动作也变成增量式的。同时还计划引入并行标记并行清理,进一步利用多核性能降低每次停顿的时间。

参考资料 #