V8垃圾回收机制
内存空间
堆和栈
调用栈中的执行上下文中存放了相应的变量环境,其中原始类型数据的数据值是直接保存在栈里,而引用类型保存的是引用地址,其值是存放在堆中。
var a=1
function f(){
console.log("今天是周五~~")
}
栈中数据回收
栈中数据的回收是通过栈顶指针ESP下移实现的
V8垃圾回收器
堆中存放数据的回收就需要用到垃圾回收器了
代际假说
代际假说(The Generational Hypothesis)的内容,这是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础之上的,所以很是重要。
代际假说有以下两个特点:
- 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
- 第二个是不死的对象,会活得更久。
其实这两个特点不仅仅适用于 JavaScript,同样适用于大多数的动态语言,如 Java、Python 等。
V8是采用分代垃圾回收机制
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。(新生代的容量比老生代小)
------------------------------------------
| 新生代 | 老生代 | V8分代
------------------------------------------
垃圾回收器工作流程
第一步,标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
第三步,做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。
新生代垃圾回收
新生代垃圾回收过程
新生代实行垃圾回收的是副垃圾回收器,使用Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域(from区),一半是空闲区域(to区)。
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
- 标记对象,留下活动对象,清除非活动对象
- 把存活的活动对象复制到空闲区
- 将对象区域和空闲区域进行翻转,原来对象区域变成空闲区域,空闲区域变成对象区域,循环反复进行。
Scavenge 算法是典型的牺牲空间换时间的方法,只利用了新生代中的对象区,但是每次都需要将存活的对象从对象区域复制到空闲区域,但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。
对象晋升
因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,新生代对象会被移动到老生区中,这就是晋升
晋升条件
对象是否经历过一次Scavenge算法(通过内存地址判断)
To空间的内存占比是否已经超过25%
老生代垃圾回收
主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。
主垃圾回收器是采用标记 - 清除(Mark-Sweep)和标记-整理(Mark-Compact)的算法进行垃圾回收的。Mark-Sweep(标记 - 清除)
顾名思义就是进行标记和清除,
标记,就是遍历堆中的对象,标记存活着的对象
清除,就是清除没有被标记的对象
但是标记清除后会存在许多内存碎片,所以还要进行整理
Mark-Compact(标记-整理)
如果学过操作系统的话应该会懂什么是内存碎片,操作系统内存管理就需要处理内存碎片
就是将存活的对象移动到一段,把空闲的碎片合并
V8垃圾回收存在的问题和解决
全停顿
由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)
这种停顿对新生代的垃圾回收影响并不大,但是对老生代影响就比较明显了,容易造成卡顿
增量标记
为了减小停顿时间带来的影响,V8 引入了增量标记,即将可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样就不会长时间无响应。