1. 为什么需要垃圾回收
我们知道JS目前的数据分为 7种基本数据类型 (Null、Undefined、Boolean、Number、String、Stymol、BigInt)和 引用数据类型 ,以及V8有两个存储数据的区域——栈空间和堆空间。
栈空间一旦被分配了就无法改变大小,它用来存储 执行上下文 ,而JS的基本数据类型就存储在执行上下文的 变量/词法环境 中。引用数据类型的大小是可变的,同时引用类型可能很大不利于栈的维护,因此 变量/词法环境 只保留引用数据类型的引用,其值被存储在可变的堆中。
数据是有生命周期的,在某个数据的生命周期结束后就应该被销毁掉,否则会一直占用内存空间,这些数据被称为垃圾数据,所以我们需要对这些垃圾数据进行回收,以释放有限的内存空间。
针对上面这张图,假设以下代码:
function print() {
var c = 'World';
var b = 'Hello';
var a = { name: 'Rskmin' };
}
print();
// print finished, todo others;
在 print 函数执行完毕后,其所分配的资源就应该被回收掉。
对于调用栈资源,我们只需要将栈顶指针下移即可,指针上方的空间都属于未分配空间,下次调用其他函数可以直接覆盖print所占用的栈空间。
对于堆资源,我们无法实现和栈一样的回收方法,因此就需要针对堆实现特定的 垃圾回收 ,也就是我们所要关注的垃圾回收机制。
2. V8是如何回收堆内存的
垃圾回收的算法有很多,针对不同的情况使用不同的回收策略才能达到最佳的效果。因此要先对内存中的垃圾进行分类,干、湿垃圾分开处理。
如何分类?V8的垃圾分类是建立在 代际假说 这个理论上的。
代际假说
-
大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问,例如上面函数中的a,函数执行完成后a变量就失去作用了。
-
不死的对象,在内存总存在的时间长,例如挂在window下的变量。
根据代际假说,堆内存中的数据大致被分为 新生代 和 老生代 ,新生区通常只支持 1~8M 的容量,而老生区的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
引用计数
- 语言引擎有一张引用表,保存了内存里面所有的资源的引用次数
- 如果一个值的引用次数是0,就可以释放这块内存
新生代垃圾回收
- 新生代对半分为两个区域,一个使用一个空闲
- 开始垃圾回收的时候,会检查(检查引用表)
FROM区域中的存活对象,如果还存活,拷贝到TO区域,完成后释放FROM区域
- 然后释放
From区域内存,再互换From和To区域(改个名)
- 当一个对象经历过多次的垃圾回收依然存活的时候,生命周期比较长的对象会被移动到老生代,这个移动过程称为晋升或升级
- 经过5次以上的回收还存在
- TO 的空间使用占比超过25%,或者超大对象
老生代垃圾回收
- 老生代使用
mark-sweep(标记清除)和mark-compact(标记整理)两种方案,大多数时候使用标记清除,空间不足以存放对象时使用标记整理 - 老生代空间大,GC耗时较长
- 在GC期间无法响应, stop-the-world
- V8优化方案:增量处理(increment-GC),把大的暂停时间分割成小的暂停时间
收集流程
- mark-sweep(标记清除 快 )[会造成内存碎片]
- 标记存活的对象
- 标记完成后清理未标记的对象
- mark-compact(标记整理 慢 )[解决内存碎片问题]
- 标记存活的对象
- 标记完成后将活着的对象移动到内存的一侧,移动完成后清除另一侧的内存
由于 标记整理法 会移动存储位置因此性能上更差一点,正常情况下使用 标记回收法 回收,当内存大小不足的时候会使用 标记整理法 整理内存碎片
由于老生代区域很大每次收集消耗的时间很长,在这段时间内(stop-the-world)浏览器无法响应,V8还对清除做了优化
增量标记法将一次回收的过程分解成多次,分解无法响应的时间并行回收启动多个辅助线程帮助回收
| 回收算法 | Mark-Sweep | Mark-Compact | Scavenge |
|---|---|---|---|
| 速度 | 中等 | 最慢 | 最快 |
| 空间开销 | 少 | 少 | 双倍空间 |
| 是否移动对象 | 否 | 是 | 是 |