JavaScript 垃圾回收机制 GC

171 阅读7分钟

总结

  • V8 的垃圾回收策略主要基于分代式垃圾回收机制

  • 新生代垃圾回收器,使用并行回收可以提高垃圾回收的效率

  • 老生代垃圾回收器中这几种策略都是融合使用的

    1. 老生代主要使用并发标记,主线程在开始执行 JavaScript 时,辅助线程也同时执行标记操作(标记操作全都由辅助线程完成)

    2. 标记完成之后,再执行并行清理操作(主线程在执行清理操作时,多个辅助线程也同时执行清理操作)

    3. 清理的任务会采用增量的方式分批在各个 JavaScript 任务之间执行

GC 即 Garbage Collection

let test = {
  name: "xianzao"
}
test = [1,2,3,4,5]
// ---
堆     	      栈
test   ->    {name: "xianzao"}
test   ->    [1,2,3,4,5]

垃圾回收策略

  • JavaScript 内存管理 可达性

    • 可访问或者说可用的值被保证存储在内存中,反之不可访问则需回收

标记清除法

  • 最常用

  • 标记 和 清除 两个阶段

    • 标记阶段即为所有活动对象做上标记

    • 清除阶段则把没有标记(也就是非活动对象)销毁

策略

  1. 垃圾收集器给所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0

  2. 从各个根对象开始遍历,把不是垃圾的节点改成1

  3. 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间

  4. 把所有内存中对象标记修改为0,等待下一轮垃圾回收

优点

  • 实现简单

缺点

  1. 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块

  2. 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢

image.png

image.png

解决方案

  • 标记整理(Mark-Compact)算法

    • 标记阶段和标记清除算法相同

    • 标记结束后,标记整理算法会将不需要清理的对象向内存的一端移动,最后清理掉边界的内存

image.png

引用计数算法

策略

  1. 声明变量,并将一个引用类型赋值给该变量时,该值的引用次数就为 1

  2. 同一个值又被赋给另一个变量,引用数加 1

  3. 该变量的值被其他的值覆盖,引用次数减 1

  4. 当这个值的引用次数变为 0 时,该值无法被访问,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

let a = new Object() 	// 此对象的引用计数为 1(a引用)
let b = a 		// 此对象的引用计数是 2(a,b引用)
a = null  		// 此对象的引用计数为 1(b引用)
b = null 	 	// 此对象的引用计数为 0(无引用)
...			// GC 回收此对象

优点

  1. 引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾

  2. 每隔一段时间进行一次,在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC

    • 标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以

缺点

  1. 需要一个计数器,而此计数器需要占很大的位置,因为不知道被引用数量的上限

  2. 循环引用无法回收

    • 循环引用,即对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A

V8对GC的优化

分代式垃圾回收

新生代垃圾回收

  • 通过 Scavenge 的算法进行垃圾回收

    • Scavenge算法采用复制式的方法 Cheney算法具体实现
    • Cheney算法 中将堆内存分为使用区和空闲区

  • 新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作

  • 垃圾回收时

    • 新生代垃圾回收器会对使用区中的活动对象做标记

    • 标记完成后将使用区的活动对象复制进空闲区并进行排序

    • 将非活动对象占用的空间清理掉

    • 角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区

  • 当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,会被移动到老生代中,采用老生代的垃圾回收策略进行管理

  • 如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中

    • 完成 Scavenge 回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配

老生代垃圾回收

  • 标记清除

    • 标记阶段

      • 从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象
    • 清除阶段

      • 老生代垃圾回收器会直接将非活动对象,也就是数据清理掉
  • V8 采用标记整理算法来解决内存碎片问题来优化空间

并行回收

  • 全停顿(Stop-The-World

    • 在进行垃圾回收时就会阻塞 JavaScript 脚本的执行,需等待垃圾回收完毕后再恢复脚本执行,我们把这种行为叫做全停顿

image.png

  • 新生代对象空间就采用并行策略

    • 启动了多个线程来清理垃圾

    • 这些线程同时将对象空间中的数据移动到空闲区域

    • 这个过程中由于数据地址会发生改变,要同步更新引用这些对象的指针,这就是并行回收

    • 全停顿式的垃圾回收方式

增量标记与懒性清理

什么是增量

  • 将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记

三色标记法(暂停与恢复)

  • 两个标记位编码三种颜色:白、灰、黑

    1. 白色指的是未被标记的对象

    2. 灰色指自身被标记,成员变量(该对象的引用对象)未被标记

    3. 黑色指自身和成员变量皆被标记

image.png

  • 直接通过当前内存中有没有灰色节点来判断整个标记是否完成

    • 如没有灰色节点,直接进入清理阶段

    • 如还有灰色标记,恢复时直接从灰色的节点开始继续执行就可以

写屏障(增量中修改引用)

  • 一旦有黑色对象引用白色对象,写屏障机制会强制将引用的白色对象改为灰色,保证下一次增量 GC 标记阶段可以正确标记,这个机制也被称作 强三色不变性

    image.png

  • 将对象 B 的指向由对象 C 改为对象 D 后,白色对象 D 会被强制改为灰色

懒性清理

  • 增量标记完成后,开始惰性清理

    • 当前的可用内存足以支撑快速的执行代码,可以延迟清理过程

    • 无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕,后面再接着执行增量标记

增量标记与惰性清理的优缺

  • 主线程的停顿时间大大减少了,让用户与浏览器交互的过程变得更加流畅

  • 增量标记缺点

    1. 并没有减少主线程的总暂停的时间,甚至会略微增加

    2. 由于写屏障机制的成本,增量标记可能会降低应用程序的吞吐量

并发回收(Concurrent)

  • 主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作

  • 辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起