无论哪种语言,我们写的代码都是作为数据存储在内存中供我们使用的,而内存总量是有限的,所以每种语言都会有相应的方式来清除那些用不到的数据来达到内存的高效利用。

对于 C/C++ 来讲,开辟空间、释放内存这种事是需要程序员自己去操作的,而像 java 与 JavaScript ,则是有一套自己垃圾回收机制,在代码执行过程中自动进行内存回收。

那么,V8 引擎是如何对 JS 代码进行垃圾回收呢?

V8 引擎的限制

首先,V8 引擎限制了可使用内存的大小。对于 64 和 32 位的系统,分别限制最大可使用内存为 1.4 G 与 0.7 G,当然,这个限制可以进行手动更改。

做这种限制出于两个原因,一是 JS 代码是单线程执行; 二是 JS 的垃圾回收机制比较耗时。因此,一旦内存过多,每次垃圾回收就会消耗更长的时间,而这个时候,正常的代码执行停滞,会带来页面卡顿等问题。

其次,V8 引擎通过分代策略,将内存分为新生代和老生代两代,新生代中的对象为存活时间较短的对象,因此这片区域也比较小, 老生代中的对象为存活时间较长或常驻内存的对象,比较大。接着对于每一代,都有更加高效的算法来进行垃圾回收。

新生代垃圾回收算法

对于新生代内存,会首先将它分为两半,分别称为 From 区与 To 区,前者是当前正在使用的区域,用来存放当前存在的对象,后者是空置的。

  1. ![image.png](https://cdn.nlark.com/yuque/0/2019/png/103124/1574692773471-08368c32-774c-4934-bbb9-d76e5bae99ab.png#align=left&display=inline&height=96&name=image.png&originHeight=288&originWidth=636&size=10495&status=done&width=212)

当垃圾回收机制开始时,V8 引擎会检查 From 区的对象,如果是存活对象,就将它复制到 To 区,当所有的存活对象都复制完成后,将整个 From 区的对象清除掉,这个算法称为Scavenge算法。这时你可能会问,那为什么不直接将 From 区中非存活的对象进行清除,要多一步复制的操作呢?这是因为我们使用内存都是连续分配的,如果只是将需要的清除掉,并不能做到真正的清除效果。

  1. ![image.png](https://cdn.nlark.com/yuque/0/2019/png/103124/1574694479791-a826ce1d-2060-4470-a83a-81a07e922f46.png#align=left&display=inline&height=179&name=image.png&originHeight=536&originWidth=1423&size=44357&status=done&width=475)

如上图所示,如果只是清除掉不需要的(绿色块),那最终我们的待分配区,也只是多了最后一格,之前的区域,都无法再使用,这些区域,我们称为内存碎片。

那如果按照 Scavenge 算法会是怎样呢?

  1. ![image.png](https://cdn.nlark.com/yuque/0/2019/png/103124/1574694958032-d70eda10-a735-41f7-98da-d8c7e4b8111f.png#align=left&display=inline&height=152&name=image.png&originHeight=457&originWidth=2010&size=47184&status=done&width=670)

由图可以看出,我们在对存活对象进行复制时,在 To 区进行了排序,这样,就能最大化的将可用空间留出来。另外,当一次回收机制执行结束,以前的 From 区成了空置的,这个时候,新的内存分配会在 To 区进行,也可以说,此时的 From 区和 To 区进行了互换,等到下一次执行时,又会换回来,就这样循环往复。

老生代垃圾回收算法

而由于新生代内存有限,所以在某些情况下,会将对象分配到老生代内存中,通常会发生在以下两个场景中:

  1. 对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一个新生代的清理,如果是,则复制到老生代中,否则复制到To空间中。
  2. 对象从From空间复制到To空间时,如果To空间已经被使用了超过25%,那么这个对象直接被复制到老生代。

上面是对于新生代内存进行垃圾回收,而对于老生代内存,则是使用 Mark-Sweep(标记清除)Mark-compact(标记整理)相结合的方式。

Mark-Sweep(标记清除)

所谓标记清除,就是首先对遍历内存中的对象,将存活对象打上标记,接着,将没有打标记的非存活对象进行清除。

这种清除方式同样会产生很多内存碎片而导致内存浪费。

Mark-Compact(标记整理)


而这一步,就是解决内存碎片的问题。在标记清除结束后,对存过对象的位置向一段进行移动,然后清除掉其余的内存。

而移动大量的对象无疑需要消耗很多时间,所以只有当标记清除后的空间不足以为新生代“晋升”的对象分配时,才会进行标记整理。

V8 引擎优化

由于垃圾回收时间过长会造成页面“全停顿”现象,V8 引擎采用一种称为“Incremental Marking(增量标记)”的方式,将标记清除的过程进行拆分,一次只清除一小部分,让垃圾回收与逻辑执行交替进行,这样使得垃圾回收的最大停顿时间缩小到了原来的 1/6。