1 介绍
JavaScript 是一种具有自动垃圾回收机制的高级编程语言。这意味着开发者不需要手动管理内存的分配和释放,JavaScript 引擎会自动跟踪内存的使用情况,并在不再需要时回收内存。
本文将详细介绍 JavaScript 的垃圾回收机制,包括其工作原理、常用的算法、可能导致的内存问题,以及如何通过代码示例来理解这些概念。
2 常见的垃圾回收算法介绍
2.1 引用计数
每个对象都会维护一个引用计数器,记录有多少引用指向它,当一个对象的引用数目为0是视为不可达,将会被回收,但是引用计数的缺点也是显而易见的,如果两个对象之间循环引用,那么两个对象即使已经没有实际作用,也会占用内存
示例
// 引用计数
const obj1 = { name: 'obj1' }
obj2 = obj1 // obj2 引用 obj1 引用计数 +1
obj1 = null // obj1 引用计数 -1
// 测试的对象仍然存在,因为 obj2 仍然引用着它
循环引用问题
// 循环引用问题
function fn(){
const obj1 = {}
const obj2 = {}
// circular reference
obj1.ref = obj2
obj2.ref = obj1
return 'fn'
}
// 调用函数obj1和obj2无法被释放
fn()
2.2 标记清除(Mark-and-Sweep)
从根节点触发(window或顶层全局对象),递归遍历所有对象,将遍历的对象标记,遍历结束后没有被标记到的对象视为不可达,将会被内存回收,这样就解决了循环引用问题。这不是立即执行的,而是周期性的执行,如果一个对象不可达,那么将在下次垃圾回收中被内存回收。
示例
准备执行垃圾回收。
从根节点开始(全局对象)递归遍历,标记可达对象,从图片可知左侧两个对象不可达,准备回收。
内存回收不可达对象,等待下一个回收周期。
2.3 分代回收机制
现在的JavaScript常用的V8引擎使用的就是分代回收机制,将内存中的对象分为了新生代和旧生代,新生代随想回收频率高,旧生代对象回收频率低,这样就大大提高了程序性能,减少了GC对性能的影响,下面的文章中将详细介绍新生代和老生代的转换以及具体垃圾回收过程。
3 V8引擎中新生代和老生代的垃圾回收机制详解
JavaScript中的内存堆(Heap)被划分为两个区域,新生代存储新创建的、声明周期较短的对象,老生代存储生命周期较长的对象,提高新生代的回收次数,处理的内存区域校,老生代回收频率低,这样就提高了程序的运行连贯性。
3.1 新生代的垃圾回收和晋升制度
3.1.1 新生代的内存结构
新生代被划分为三个区域,一个Eden空间,新对象总是现在这里被分配内存,以及两个等大小的Survivor空间,一个称为From空间,另一个成为To空间,其中一个总是空闲的,用于在垃圾回收时接收存活的对象。
3.1.2 小型垃圾回收(Minor GC)
Minor GC时新生代空间专门的垃圾回收规则,步骤如下:
- 标记和赋值:从根节点(root)开始,标记所有可达对象,然后将Eden和From空间中的存活的对象复制到To空间,空间不足时会导致晋升到老生代,待会会详细说明。
- 对象年龄计数:对象每经过依次GC,年龄就会加一,达到阈值后提升到老生代。
- 交换空间:回收Eden和From空间的所有对象,交换From和To空间的角色,为下一次GC做准备。
3.1.3 新生代的对象晋升条件和机制
- 对象年龄达到阈值:多次Minor GC后仍然存活,条件达到阈值,复制到老生代。
- Survivor空间不足:在复制Eden和From对象到To空间是发现空间不足,那么直接将剩余存活对象晋升到老生代。
- 大对象直接分配老生代:占用大量内存的对象,如大型数组和字符串将会直接分配到老生代,为了避免频繁移动带来的大损耗。
3.2 老生代的垃圾回收机制 Major GC(大型垃圾回收)
Major GC时争对老生代的垃圾回收机制,通常采用标记-清除(Mark-Sweep)和标记-整理(Mark-Compact)算法。
3.2.1 标记清除(Mark-Sweep)算法
和在第二节中所讲的标记清除算法一直,从根对象开始标记所有可达对象,然后遍历整个老生代空间,回收未被标记的对象所占用的空间,但是这样下来就会产生大量离散的内存碎片,导致大对象无法分配。
3.2.2 标记整理(Mark-Compact)算法
为了解决在标记清除阶段标记清除阶段产生的内存碎片,会有以下两个步骤:
- 标记阶段:和标记清除一致
- 整理阶段:将存活对象向一端移动,挪出内存空间
3.2.3 老生代垃圾回收的触发条件
- 老生代空间不足时
- 手动调用(Node.js环境中调用global.gc())
- 调用定时器时可能触发
4 垃圾回收的优化与并行回收
4.1 增量式回收
为了减少垃圾回收的事件,将垃圾回收过程分解为多个小步骤,穿插在程序执行中。
4.2 并行和并发回收
利用多核CPU,同时运行多个回收线程,同时垃圾回收线程和程序线程同时运行。
5 弱引用(WeakMap和WeakSet)在垃圾回收中的应用
弱引用是一种特殊的引用类型,弱引用的对象不会阻止垃圾回收该对象,一般情况变量对对象的引用都是强引用,如果强引用不存在了,那么垃圾回收器就会回收该对象。
5.1 WeakMap和WeakSet的特性
1. WeakMap
- 健(key)必须是对象,不能是原始类型,值(value)可以是任意类型。
- 对象弱引用
- 没有实现迭代器协议,不能被遍历,不能使用for of keys() values() entries()
2.WeakSet
- 元素必须是对象
- 对象弱引用
- 不可遍历,且没有size方法
5.2 WeakMap在响应式系统(Vue)中的应用-依赖收集与存储
需要对每个响应式对象存储器依赖的观察者,当对象变化时,通知这些对象,在这里使用WeakMap当响应式对象不再被引用时,相关的发布订阅关系自动解除。实现需要为每一对象船舰一个依赖管理器,存储订阅者列表,然后使用WeakMap将对象映射到对应的Dep依赖管理器上,实现的代码如下:
// 创建一个 WeakMap,用于存储响应式对象与其依赖关系
const targetMap = new WeakMap();
function track(target, key) {
// 获取或创建该对象的依赖管理器
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取或创建该属性的订阅者集合
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 将当前的订阅者添加到集合中
dep.add(activeEffect);
}
function trigger(target, key) {
// 获取该对象的依赖管理器
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 获取该属性的订阅者集合
const dep = depsMap.get(key);
if (dep) {
// 通知所有订阅者
dep.forEach(effect => effect());
}
}
5.3 WeakSet在响应式系统(Vue)中的应用-循环引用的检测
在深度遍历对象或处理嵌套的响应式结构的时候,可能会遇到循环引用,在递归遍历对象时,使用 WeakSet 存储已访问的对象,如果再次发现访问过的对象,那么停止进行额外处理。
const seen = new WeakSet()
function traverse(value, seen = new WeakSet()) {
if (typeof value !== 'object' || value === null || seen.has(value)) {
return;
}
seen.add(value);
for (const key in value) {
traverse(value[key], seen);
}
}
5.4 具体应用案例-reactive函数的实现
在 Vue 3 中,reactive 函数用于将普通对象转换为响应式对象。为确保同一个对象始终返回相同的代理对象,Vue 使用了 WeakMap 来缓存对象与其代理之间的映射。
思路如下:创建一个 WeakMap,键为原始对象,值为其对应的代理对象。 在调用 reactive 时,先检查缓存中是否已有对应的代理对象,如果有,直接返回。
const reactiveMap = new WeakMap();
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
// 检查是否已存在代理
let existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 创建代理对象
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key);
const result = Reflect.get(target, key, receiver);
return reactive(result); // 深度响应式
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 触发更新
trigger(target, key);
return result;
}
});
// 缓存代理对象
reactiveMap.set(target, proxy);
return proxy;
}