Published on

V8的垃圾回收策略

Authors
  • avatar
    Name
    Joy Peng
    Twitter

Introduction

垃圾回收的思想很简单,有些数据被使用之后可能不再需要了,这就产生了垃圾数据,我们需要回收这些垃圾数据,释放内存空间。

不同语言的垃圾回收策略

通常情况下,垃圾回收策略分为

  • 手动回收
  • 自动回收

在C/C++等语言中,何时分配、销毁内存都需要程序员手动管理,这就是手动回收。如果一段数据已经不再需要了,但是程序员忘记释放内存,就会造成内存泄漏。 在Java、JavaScript等语言中,有自动回收机制,当一段数据不再被引用时,垃圾回收器会自动回收这段数据,释放内存空间。

JavaScript调用栈(call stack)中的数据如何回收?

以这段代码为例:

function foo(){
    var a = 1
    var b = {name:"极客邦"}

    function showName(){
        var c = 2 var d = {name:"极客时间"}
    }
    showName()
}
foo()

执行顺序如下:

  • 首先在全局执行上下文中,创建了一个foo函数,然后调用foo函数,创建了foo函数的执行上下文,执行foo函数中的代码。
  • foo函数的执行上下文的变量环境中,创建了变量ab,然后调用showName函数,创建了showName函数的执行上下文,执行showName函数中的代码。
  • showName函数的执行上下文的变量环境中,创建了变量cd
  • showName函数执行完毕,showName函数的执行上下文被销毁foo函数继续执行。

这个被销毁是怎么做的呢?在调用栈中,有一个记录当前执行状态的指针ESP(栈指针),当一个函数执行完毕,这个指针会向下移动,指向下一个函数的执行上下文。这样,当前函数的执行上下文就被销毁了。如下图:

函数的执行上下文被销毁后,变量环境中的变量ac也会被销毁,这样就释放了内存空间,因为这些变量是存储在栈内存中的。 但变量bd还存在于堆内存中,这时候就需要垃圾回收器来回收这些数据。

V8的垃圾回收策略

代际假说和分代收集

V8引擎的垃圾回收策略是基于代际假说和分代收集的。代际假说认为,新生的对象更有可能很快被回收,而存活时间较长的对象可能存活更久。

因此,V8将内存分为新生代老生代两个区域,分别使用不同的垃圾回收算法。

  • 新生代:存放生存时间短的对象,通常只支持1~8M的容量。使用Scavenge算法。由副垃圾回收器负责回收。
  • 老生代:存放生存时间长的对象,使用Mark-SweepMark-Compact算法。由主垃圾回收器负责回收。

副垃圾回收器

副垃圾回收器负责新生区的垃圾回收,采用Scavenge算法,Scavenge算法是一种基于复制的垃圾回收算法,将新生代内存空间分为两个区域

  • From Space
  • To Space

From区域的内存空间占满时,就会触发垃圾回收,将存活的对象复制到To区域,然后清空From区域。这样就把垃圾数据清理掉了。

将存活对象复制到To区域的时候是顺序复制的,这样可以保证内存空间的连续性,提高内存的利用率。完成复制后,From区域和To区域的角色会互换,这样就完成了一次垃圾回收。

TIP

新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

主垃圾回收器

主垃圾回收器负责老生区的垃圾回收,采用Mark-Sweep和Mark-Compact算法。老生区中的对象有两个特点

  • 存活时间长
  • 占用空间大

除了新生区中晋升的对象,一些大的对象会直接被分配到老生区,因为新生区使用的Scavenge算法需要进行复制操作,大对象会花费很多时间。

Mark-Sweep算法

Mark-Sweep算法分为两个阶段,标记阶段和清除阶段。

  • 标记阶段:从根节点出发,遍历所有对象,在这个过程中,能到达的对象都会被标记为live,没有被标记的对象就是dead
  • 清除阶段:清除所有没有被标记的对象,这样就完成了一次垃圾回收。

Mark-Compact算法

由于在Mark-Sweep算法中,清除后会产生内存碎片,为了解决这个问题,引入了Mark-Compact算法。

  • 标记阶段:和Mark-Sweep算法一样,标记所有存活的对象。
  • 压缩阶段:将存活的对象向一端移动,然后清理掉边界外的内存空间,这样就解决了内存碎片的问题。

全停顿(Stop-The-World)

由于JavaScript是单线程的,所以在垃圾回收的时候,会阻塞整个线程。例如堆中有1.5GB的数据,V8进行一次完整的垃圾回收需要1秒以上的时间,这时候整个线程就会被阻塞,用户的操作就会卡顿。

这就是全停顿

为了降低全停顿的时间,V8引入了增量标记算法,将标记阶段分为多个小步骤,每执行完一个小步骤就让出线程,这样就可以让用户操作和垃圾回收交替进行,减少卡顿的时间。

总结

V8的垃圾回收策略是基于代际假说和分代收集的,通过不同的算法来回收新生区和老生区的内存。在垃圾回收的过程中,会产生全停顿,为了降低全停顿的时间,V8引入了增量标记算法。

TIP

如何判断内存泄露?内存泄露是指一块内存空间不再被使用,但是没有被释放。在JavaScript中,内存泄露通常是因为一些对象被长时间引用,导致垃圾回收器无法回收这些对象。 可以通过chrome的Performance工具来检测内存泄露,查看内存的变化情况,找出内存泄露的原因。

参考

[1] https://time.geekbang.org/column/intro/100033601