V8将堆分为两个区域——新生代和老生代,分别使用不同的垃圾回收器。不过其实不论什么类型的垃圾回收器它们的执行流程都是一样的。
第一步:标记空间中的活动对象与非活动对象,活动对象指的是还在使用的对象,而非活动对象指的是可以进行垃圾回收的对象。
第二步:回收非活动对象所占据的内存。其实就是在所有标记完成后,统一清理掉内存中所有标记为可回收的对象。
第三步:内存整理。因为频繁的回收对象后,内存中就会存在大量不连续的空间,这些不连续的内存空间称之为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就可能出现内存不足的情况,所以最后一步就是整理。不过有的垃圾回收器不会产生内存碎片。
新生代(副垃圾回收器):
多数小的对象都会被分配到新生区,新生代中用Scavenge算法来处理,所谓Scavenge算法就是把新生代空间分为两个区域,一半为对象区,一半为空闲区。
新加入的对象都会存放到对象区,当对象区快被写满时,就需要执行一次垃圾清理。
在垃圾回收过程中,首先对对象区的垃圾做标记;标记完成后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区,同时还会把这些对象有序的排列起来,所以这个复制的过程,就相当于完成了内存整理,复制后的空闲区就没有垃圾碎片了。
完成复制后将空闲区与对象区进行角色翻转,这样就完成了垃圾对象回收操作,同时角色翻转的操作还能使新生代中两块区域无限重复使用。
不过由于新生代采用的Scavenge算法所以执行清理操作时,都需要将存活的对象从对象区复制到空闲区。但复制的操作需要时间成本,如果新生区空间设置太大了,那么每次复制的时间就会久,所以为了执行效率,一般新生代的空间会被设置得比较小。
也正是因为空间设置得比较小,所以很容易被存活的对象装满整个区域,为了解决这个问题JS引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区。
老生代(主垃圾回收器):
主垃圾回收器主要负责老生区的垃圾回收。除了新生区晋升的对象,一些大的对象也会直接被分配到老生区。因此老生区中的对象有两个特点一个是对象占用空间大,另一个就是对象存活时间长。
由于老生区对象比较大,若要在老生区中使用Scavenge算法进行垃圾回收,复制这些大的对象会花费比较多的时间,从而导致回收执行效率不高,还会浪费掉一半的空间。因此主垃圾回收器采用标记——清除算法
首先是标记阶段,标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称之为活动对象,没有到达的就可以判断为垃圾数据。然后清除掉垃圾数据
不过采用标记清除算*产生大量的不连续内存碎片。而碎片过多会导致大对象无法分配到内存,于是产生了标记整理算法它的标记过程和标记清除算法是一样的,但清理过程不是直接清理而是将存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
全停顿:
由于JavaScript是运行在主线程上的,一旦执行垃圾回收算法,都需要将正在执行的JavaScript脚本暂停,待回收完毕后在恢复执行脚本,这种现象叫做全停顿。
为了降低这种卡顿,V8将标记过程分为一个个子标记过程,同时让垃圾回收标记和JavaScript应用逻辑交替进行,知道标记阶段完成,我们把这个算法成为增量标记算法
在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行