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 的过程,辅助线程能够在后台完成的执行垃圾回收的操作。
- 主垃圾回收器就综合采用了所有的方案(并发标记,增量标记,辅助清理),副垃圾回收器也采用了部分方案。