Skip to content

V8 原理之垃圾回收

垃圾回收

V8的垃圾回收策略主要是基于分代式垃圾回收机制,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法。

世代假说

世代假说(generational hypothesis),也称为弱分代假说(weak generational hypothesis)。这个假说表明,大多数新生的对象在分配之后就会死亡,而老的对象通常倾向于在程序运行周期中永存。

V8的内存结构

在V8引擎的堆结构组成中,其实除了新生代老生代外,还包含其他几个部分,但是垃圾回收的过程主要出现在新生代和老生代,所以对于其他的部分我们没必要做太多的深入,V8的内存结构主要由以下几个部分组成:

  • 新生代(new_space):大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。
  • 老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。
  • 大对象区(large_object_space):存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。
  • 代码区(code_space):代码对象,会被分配在这里,唯一拥有执行权限的内存区域。
  • map区(map_space):存放Cell和Map,每个区域都是存放相同大小的元素,结构简单。

新生代

在V8引擎的内存结构中,新生代主要用于存放存活时间较短的对象。新生代内存是由两个semispace(半空间)构成的,内存最大值在64位系统和32位系统上分别为32MB和16MB,在新生代的垃圾回收过程中主要采用了Scavenge算法。

Scavenge算法的中,它将新生代内存一分为二,每一个部分的空间称为semispace,其中处于激活状态的区域我们称为From空间,未激活(inactive new space)的区域我们称为To空间。

这两个空间中,始终只有一个处于使用状态,另一个处于闲置状态。

我们的程序中声明的对象首先会被分配到From空间,当进行垃圾回收时,如果From空间中尚有存活对象,则会被复制到To空间进行保存,非存活的对象会被自动回收。

当复制完成后,From空间和To空间完成一次角色互换。

对象晋升

当一个对象在经过多次复制之后依旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被直接转移到老生代中,这种对象从新生代转移到老生代的过程我们称之为晋升。 对象晋升的条件主要有以下两个:

  • 对象是否经历过一次Scavenge算法
  • To空间的内存占比是否已经超过25%

老生代

在老生代中,因为管理着大量的存活对象,如果依旧使用Scavenge算法的话,很明显会浪费一半的内存,因此已经不再使用Scavenge算法,而是采用新的算法Mark-Sweep(标记清除)Mark-Compact(标记整理)来进行管理。

  • 引用计数

    • 在早前我们可能听说过一种算法叫做引用计数:看对象是否还有其他引用指向它,如果没有指向该对象的引用,则该对象会被视为垃圾并被垃圾回收器回收。
    • 但是一旦我们碰到循环引用的场景,就会出现问题。
    • 截至 2012 年所有的现代浏览器均放弃了引用计数这种算法,转而采用新的Mark-Sweep(标记清除)Mark-Compact(标记整理)算法。
  • Mark-Sweep(标记清除)

    • Mark-Sweep(标记清除)分为标记清除两个阶段,在标记阶段会遍历堆中的所有对象,然后标记活着的对象,在清除阶段中,会将死亡的对象进行清除。具体步骤如下:
    • 1、垃圾回收器会在内部构建一个根列表,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window全局对象可以看成一个根节点。
    • 2、然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
    • 3、最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。
    • Mark-Sweep 算法存在一个问题,就是在经历过一次标记清除后,因为我们所清理的对象的内存地址可能不是连续的,所以就会出现内存碎片。为了解决这种内存碎片的问题,Mark-Compact(标记整理)算法被提了出来.
    • 为了解决这种内存碎片的问题,Mark-Compact(标记整理)算法被提了出来。
  • Mark-Compact(标记整理)

    • 回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存。

Stop The World

  • 由于 JS 的单线程机制,垃圾回收的过程会阻碍主线程同步任务的执行。因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。
  • 这种行为叫做全停顿(Stop-The-World)
  • 为了减少垃圾回收带来的停顿时间,V8引擎又引入了 Incremental Marking(增量标记)
  • 得益于增量标记的好处,V8引擎后续继续引入了延迟清理(lazy sweeping)增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。

V8 最开始的垃圾回收器有两个特点:

  • 第一个是垃圾回收在主线程上执行,
  • 第二个特点是一次执行一个完整的垃圾回收流程。

由于这两个原因,很容易造成主线程卡顿,所以 V8 采用了很多优化执行效率的方案:

  • 第一个方案是 并行回收,在执行一个完整的垃圾回收过程中,垃圾回收器会使用 多个辅助线程 来并行执行垃圾回收。
  • 第二个方案是 增量式垃圾回收,垃圾回收器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。采用增量垃圾回收时,垃圾回收器没有必要一次执行完整的垃圾回收过程,每次执行的只是整个垃圾回收过程中的一小部分工作。
  • 第三个方案是并发回收,回收线程在执行 JavaScript 的过程,辅助线程能够在后台完成的执行垃圾回收的操作。
  • 主垃圾回收器就综合采用了所有的方案(并发标记,增量标记,辅助清理),副垃圾回收器也采用了部分方案。

如何避免内存泄漏

尽可能少地创建全局变量

手动清除定时器

少用闭包

清除DOM引用

弱引用

References

问题:浏览器 V8 是如何工作的?