- Published on
V8的垃圾回收策略
- Authors
- Name
- Joy Peng
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
函数的执行上下文的变量环境中,创建了变量a
和b
,然后调用showName
函数,创建了showName
函数的执行上下文,执行showName
函数中的代码。 - 在
showName
函数的执行上下文的变量环境中,创建了变量c
和d
。 showName
函数执行完毕,showName
函数的执行上下文被销毁,foo
函数继续执行。
这个被销毁是怎么做的呢?在调用栈中,有一个记录当前执行状态的指针ESP(栈指针),当一个函数执行完毕,这个指针会向下移动,指向下一个函数的执行上下文。这样,当前函数的执行上下文就被销毁了。如下图:
函数的执行上下文被销毁后,变量环境中的变量a
和c
也会被销毁,这样就释放了内存空间,因为这些变量是存储在栈内存中的。 但变量b
和d
还存在于堆内存中,这时候就需要垃圾回收器来回收这些数据。
V8的垃圾回收策略
代际假说和分代收集
V8引擎的垃圾回收策略是基于代际假说和分代收集的。代际假说认为,新生的对象更有可能很快被回收,而存活时间较长的对象可能存活更久。
因此,V8将内存分为新生代和老生代两个区域,分别使用不同的垃圾回收算法。
- 新生代:存放生存时间短的对象,通常只支持1~8M的容量。使用Scavenge算法。由副垃圾回收器负责回收。
- 老生代:存放生存时间长的对象,使用Mark-Sweep和Mark-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
工具来检测内存泄露,查看内存的变化情况,找出内存泄露的原因。