垃圾回收(一):V8的两个垃圾回收器是如何工作的?

垃圾数据是怎么产生的?

首先,我们看看垃圾数据是怎么产生的。
无论是使用什么语言,我们都会频繁地使用数据,这些数据会被存放到栈和堆中,通常的方式是在内存中创建一块空间,使用这块空间,在不需要的时候回收这块空间。

window.test = new Object()
window.test.a = new Uint16Array(100)

当JavaScript执行这段代码的时候,会先为window对象添加一个test属性,并在堆中创建了一个空对象,并将该对象的地址指向了window.test属性。随后又创建一个大小为100的数组,并将属性地址指向了test.a的属性值。此时的内存布局图如下所示:
image.png
我们可以看到,栈中保存了指向window对象的指针,通过栈中window的地址,我们可以到
达window对象,通过window对象可以到达test对象,通过test对象还可以到达a对象。
如果此时,我将另外一个对象赋给了a属性,代码如下所示:

window.test.a = new Object()

image.png
我们可以看到,a属性之前是指向堆中数组对象的,现在已经指向了另外一个空对象,那么此时数组对象就成为了垃圾数据。

垃圾回收算法

第一步,通过GC Root标记空间中活动对象和非活动对象。
目前V8采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。

  • 通过GC Root遍历到的对象,我们就认为该对象是可访问的(reachable),那么必须保证这些对象应该在内存中保留,我们也称可访问的对象为活动对象;
  • 通过GC Roots没有遍历到的对象,则是不可访问的(unreachable),那么这些不可访问的对象就可能被回收,我们称不可访问的对象为非活动对象。
    第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
    第三步,做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。
    目前V8采用了两个垃圾回收器,主垃圾回收器-Major GC和副垃圾回收器-Minor GC (Scavenger)。V8之所以使用了两个垃圾回收器,主要是受到了代际假说(The Generational Hypothesis)的影响。
    代际假说是垃圾回收领域中一个重要的术语,它有以下两个特点:
  • 第一个是大部分对象都是“朝生夕死”的,也就是说大部分对象在内存中存活的时间很短,比如函数内部声明的变量,或者块级作用域中的变量...
  • 第二个是不死的对象,会活得更久,比如全局的window、DOM、Web API等对象。

在V8中,会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象。
新生代通常只支持1~8M的容量,而老生代支持的容量就大很多了。对于这两块区域,V8分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
副垃圾回收器-Minor GC (Scavenger),主要负责新生代的垃圾回收。
主垃圾回收器-Major GC,主要负责老生代的垃圾回收。

副垃圾回收器

副垃圾回收器主要负责新生代的垃圾回收。通常情况下,大多数小的对象都会被分配到新生代,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的。
新生代中的垃圾数据用Scavenge算法来处理。所谓Scavenge算法,是把新生代空间对半划分为两个区域,一半是对象区域(from-space),一半是空闲区域(to-space),如下图所示:

新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
在垃圾回收过程中,先对对象区域垃圾做标记,然后副垃圾回收器会将存活的对象复制到空闲区域,同时会有序的排列起来,这个复制过程,也就相当于完成了内存整理操作。
完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时,这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
为了执行效率,一般新生区的空间会被设置得比较小。
也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域,副垃圾回收器一旦监控对象装满了,便执行垃圾回收。
同时,副垃圾回收器还会采用对象晋升策略。

主垃圾回收器

主垃圾回收器主要负责老生代中的垃圾回收。除了新生代中晋升的对象,一些大的对象会直接被分配到老生代里。因此,老生代中的对象有两个特点:

  • 一个是对象占用空间大;
  • 另一个是对象存活时间长。
    主垃圾回收器是采用标记-清除(Mark-Sweep)的算法进行垃圾回收的。
    首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
    接下来就是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程完全不同,主垃圾回收器会直接将标记为垃圾的数据清理掉。
    对垃圾数据进行标记,然后清除,这就是标记-清除算法,不过对一块内存多次执行标记-清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存,于是又引入了另外一种算法——标记-整理(Mark-Compact)。
    这个算法的标记过程仍然与标记-清除算法里的是一样的,先标记可回收对象,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉这一端之外的内存。你可以参考下图:
此文章为5月Day26学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪

豪猪
4 声望4 粉丝

undefined