研究JS的垃圾回收机制,其实就是研究JS所运行时环境的内存管理机制。从原生JS的垃圾回收机制,再到V8引擎所使用的新的回收机制,每一种机制都值得拿出来探究,本文以图文的形式争取让大家理解。
垃圾回收常见的几个算法(机制)
- 引用计数
- 标记清除
- 标记清除整理
- Cheney算法:目前V8引擎新生代垃圾回收
我们来逐个讲解。
JS的垃圾回收机制
Js的垃圾回收机制主要分为两个:
- 标记清除法
- 引用计数法
前置概念:什么是循环引用:
循环引用非彼for循环。
循环引用指的是两个或多个对象之间相互引用对方,形成一个闭环的引用链。举例说,对象 A 引用了对象 B,而对象 B 也引用了对象 A,这样就形成了循环引用。
js
// 定义一个构造函数 Node 表示节点
function Node() {
this.neighbors = []
}
// 实例两个节点: left 和 right
let left = new Node()
let right = new Node()
// 循环引用
left.neighbors.push(right)
right.neighbors.push(left)
// 现在 left 和 right 形成了循环引用
// 即使将它们设置为 null,也无法解除循环引用
left = null
right = null
标记清除法
这是Js垃圾回收机制中最常用的一种算法。
-
阶段一: 标记阶段
- 垃圾回收器会从跟对象(可以理解为全局对象和当前执行上下文的局部变量?)开始,遍历所有。
- 在遍历的过程中,会先给所有对象来标记一个状态,假设 1 所示在使用。
-
阶段二:清除标记
- 垃圾回收器会检查一边所有的对象(遍历),如果发现某个对象没被标记为 1 而是 0 (没被使用)
- 它会把所有标记为 0 的对象删除,释放内存。
这种方法的优点是能够有效处理循环引用的问题,因为只要对象被标记为 0 ,就会被回收,不管在不在循环体中。
引用计数
引用计数是一种较为简单的垃圾回收策略,因为简单,所以天然但存在一些缺陷,对于一些循环引用的对象难以处理。
-
阶段一: 引用计数机制:
- 每个对象都有一个引用计数器属性(在原型链上),这个属性记录有多少个其他对象在引用它。
- 当一个新引用指向该对象时,引用计数器加一;当一个引用被删除时,引用计数器减一。
-
阶段二:回收阶段
- 对于哪些引用计数为 0 的对象,js就会判定他为未被引用,然后删除它。
无论是标记清除还是引用计数法,都是原生的JS的垃圾回收机制。他们天然存在一些缺陷。
- 引用计数法: 对循环引用无法处理。
- 标记清除法:存在暂停时间长、内存碎片以及非实时性问题。
上面既然说了 原生Js 对于垃圾回收机制存在缺陷 ,那么这些问题要怎么解决?其实强大的 V8引擎 就提出了新的方法和管理模式来对内存进行更准确地管理
V8 引擎中的垃圾回收机制
V8 引擎是 Google 开发的高性能 JavaScript 和 WebAssembly 引擎,广泛应用于 Chrome 浏览器和 Node.js 中。为了提升垃圾回收的效率和减少对应用程序性能的影响,V8 引擎对 JavaScript 的垃圾回收机制进行了多种优化。
关于V8有很多的地方可以研究,我们就来探究它对于垃圾回收的优化。
分代垃圾回收
V8引擎把内存划分为两块(Chenney算法):新生代、老生代。
-
新生代
- 存放一些生命周期比较短的对象。
- 用了特殊的算法:通过复制收集机制,将还在存活的对象从一个空间复制到另外一个空间,没有被复制的对象(未使用的)就会被回收。
- 这个特殊的算法执行时间短,成本低,新生代对象存活时间比较短,回收频率高。
-
老生代
- 存放一些生命周期较长的对象。
- 使用标记-清除和标记-压缩算法。
- 标记-清除:遍历并标记活跃对象,然后清除未标记的对象。
- 标记-压缩:在清除阶段后进行内存整理,将存活对象压缩到内存的一端,以减少内存碎片。
很抽象,较难理解。。。
- 初始时,新生代空间存在
1,2,3
三个对象,占用空间大小我们用矩形面积来表示。这个时候新增变量4
,但是原先内存空间因为不连续,导致4
放不下。
- 于是,复制算法作用下,将新生代和即将加入的 4 复制放入到 老生代空间,并用进行了重排。
- 复制收集完成后,(交换空间)将老生代和新生代互换,等下下次对象的变化。
以上只是新生代和老生代转变重排的示例,在接下来的循环中,新老生代中复制收集算法、标记清除算法同时生效。将不活跃的老生代清除掉,来释放空间。
V8在垃圾回收时用到的机制:
- 增量标记:将标记阶段分成多个小步骤,逐步完成标记过程。
- 并发标记: 在后台线程中执行标记操作,不阻塞主线程。
- 延迟清理: 在标记阶段完成后,并不立即进行全部清理操作。而是将清理操作分散到后续的内存分配过程中,这样可以平摊清理成本,避免一次性清理带来的性能开销。
- 并行 清理: 在多个线程中并行执行清理操作。
- 内存 压缩: 将存活对象移动到内存的另一端,释放出连续的大块内存空间。
Cheney算法
上面说到,V8把内存划分为两块,其实这其中用到了Chenney算法:一种高效的垃圾回收算法,属于复制收集(Copying Collection)技术的一种。
Cheney算法将内存分成两个半空间,分别称为"从空间"(From-space)和"到空间"(To-space)。
浅谈Cheney算法原理
-
初始化: 所有对象最初都分配在"从空间"中。
-
复制阶段:从根对象开始,遍历所有可达对象,将它们从"从空间"复制到"到空间"。
-
更新阶段: 在复制过程中,更新所有对象的引用,以确保它们指向"到空间"中的新对象。
-
交换空间: 完成复制后,交换"从空间"和"到空间"的角色,原来的"到空间"变为新的"从空间",所有存活对象现在都在这个空间中,原来的"从空间"则可以完全清空。