总结
-
V8 的垃圾回收策略主要基于分代式垃圾回收机制
-
新生代垃圾回收器,使用并行回收可以提高垃圾回收的效率
-
在老生代垃圾回收器中这几种策略都是融合使用的
-
老生代主要使用并发标记,主线程在开始执行 JavaScript 时,辅助线程也同时执行标记操作(标记操作全都由辅助线程完成)
-
标记完成之后,再执行并行清理操作(主线程在执行清理操作时,多个辅助线程也同时执行清理操作)
-
清理的任务会采用增量的方式分批在各个 JavaScript 任务之间执行
-
GC 即 Garbage Collection
let test = {
name: "xianzao"
}
test = [1,2,3,4,5]
// ---
堆 栈
test -> {name: "xianzao"}
test -> [1,2,3,4,5]
垃圾回收策略
-
JavaScript 内存管理
可达性- 可访问或者说可用的值被保证存储在内存中,反之不可访问则需回收
标记清除法
-
最常用
-
标记 和 清除 两个阶段
-
标记阶段即为所有活动对象做上标记
-
清除阶段则把没有标记(也就是非活动对象)销毁
-
策略
-
垃圾收集器给所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
-
从各个根对象开始遍历,把不是垃圾的节点改成1
-
清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
-
把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点
- 实现简单
缺点
-
内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
-
分配速度慢,因为即便是使用
First-fit策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
解决方案
-
标记整理(Mark-Compact)算法
-
标记阶段和标记清除算法相同
-
标记结束后,标记整理算法会将不需要清理的对象向内存的一端移动,最后清理掉边界的内存
-
引用计数算法
策略
-
声明变量,并将一个引用类型赋值给该变量时,该值的引用次数就为 1
-
同一个值又被赋给另一个变量,引用数加 1
-
该变量的值被其他的值覆盖,引用次数减 1
-
当这个值的引用次数变为 0 时,该值无法被访问,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
let a = new Object() // 此对象的引用计数为 1(a引用)
let b = a // 此对象的引用计数是 2(a,b引用)
a = null // 此对象的引用计数为 1(b引用)
b = null // 此对象的引用计数为 0(无引用)
... // GC 回收此对象
优点
-
引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾
-
每隔一段时间进行一次,在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC
- 标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以
缺点
-
需要一个计数器,而此计数器需要占很大的位置,因为不知道被引用数量的上限
-
循环引用无法回收
- 循环引用,即对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A
V8对GC的优化
分代式垃圾回收
新生代垃圾回收
-
通过
Scavenge的算法进行垃圾回收Scavenge算法采用复制式的方法Cheney算法具体实现
Cheney算法 中将堆内存分为使用区和空闲区
-
新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作
-
垃圾回收时
-
新生代垃圾回收器会对使用区中的活动对象做标记
-
标记完成后将使用区的活动对象复制进空闲区并进行排序
-
将非活动对象占用的空间清理掉
-
角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区
-
-
当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,会被移动到老生代中,采用老生代的垃圾回收策略进行管理
-
如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中
- 完成
Scavenge回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配
- 完成
老生代垃圾回收
-
标记清除
-
标记阶段
- 从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象
-
清除阶段
- 老生代垃圾回收器会直接将非活动对象,也就是数据清理掉
-
-
V8 采用标记整理算法来解决内存碎片问题来优化空间
并行回收
-
全停顿(
Stop-The-World)- 在进行垃圾回收时就会阻塞 JavaScript 脚本的执行,需等待垃圾回收完毕后再恢复脚本执行,我们把这种行为叫做全停顿
-
新生代对象空间就采用并行策略
-
启动了多个线程来清理垃圾
-
这些线程同时将对象空间中的数据移动到空闲区域
-
这个过程中由于数据地址会发生改变,要同步更新引用这些对象的指针,这就是并行回收
-
全停顿式的垃圾回收方式
-
增量标记与懒性清理
什么是增量
- 将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记
三色标记法(暂停与恢复)
-
两个标记位编码三种颜色:白、灰、黑
-
白色指的是未被标记的对象
-
灰色指自身被标记,成员变量(该对象的引用对象)未被标记
-
黑色指自身和成员变量皆被标记
-
-
直接通过当前内存中有没有灰色节点来判断整个标记是否完成
-
如没有灰色节点,直接进入清理阶段
-
如还有灰色标记,恢复时直接从灰色的节点开始继续执行就可以
-
写屏障(增量中修改引用)
-
一旦有黑色对象引用白色对象,写屏障机制会强制将引用的白色对象改为灰色,保证下一次增量 GC 标记阶段可以正确标记,这个机制也被称作 强三色不变性
-
将对象 B 的指向由对象 C 改为对象 D 后,白色对象 D 会被强制改为灰色
懒性清理
-
增量标记完成后,开始惰性清理
-
当前的可用内存足以支撑快速的执行代码,可以延迟清理过程
- 无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕,后面再接着执行增量标记
-
增量标记与惰性清理的优缺
-
主线程的停顿时间大大减少了,让用户与浏览器交互的过程变得更加流畅
-
增量标记缺点
-
并没有减少主线程的总暂停的时间,甚至会略微增加
-
由于写屏障机制的成本,增量标记可能会降低应用程序的吞吐量
-
并发回收(Concurrent)
-
主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作
-
辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起