17. 说说你对垃圾回收机制的理解?
问题解析
JavaScript具有自动垃圾回收机制(Garbage Collection,简称GC),程序员不需要手动管理内存的分配和释放。理解垃圾回收的工作原理、算法以及内存管理策略,对于编写高性能、无内存泄漏的代码至关重要。
核心概念
什么是垃圾回收
垃圾回收是自动内存管理的一种形式,垃圾回收器会定期找出那些不再继续使用的变量,然后释放其占用的内存。
内存生命周期
- 分配内存:声明变量时自动分配
- 使用内存:读写变量
- 释放内存:垃圾回收器自动回收不再使用的内存
详细解答
垃圾回收的两种主要算法
1. 标记清除(Mark-and-Sweep)
JavaScript最常用的垃圾回收机制。
工作原理:
- 垃圾回收器运行时,会标记内存中存储的所有变量
- 将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉
- 在此之后再被加上标记的变量就是待删除的(无法从上下文访问)
- 垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存
function createData() {
let data = { name: 'test', value: [1, 2, 3] };
return data;
}
let ref = createData();
// ref还在引用data,data不会被回收
ref = null;
// ref不再引用data,data在下次垃圾回收时会被回收
2. 引用计数(Reference Counting)
工作原理:
- 语言引擎有一张"引用表",保存了内存中所有资源的引用次数
- 引用次数为0时,表示这个值不再用到,可以释放内存
// 数组[1, 2, 3, 4]的引用次数为1
let arr = [1, 2, 3, 4];
// 引用次数变为2
let arr2 = arr;
// 引用次数变为1
arr = null;
// 引用次数变为0,可以被垃圾回收
arr2 = null;
引用计数的问题:循环引用
function problem() {
let objA = {};
let objB = {};
objA.ref = objB; // objB的引用次数为2
objB.ref = objA; // objA的引用次数为2
// 函数执行完毕,objA和objB的引用次数各减1,但仍为1
// 无法被回收,造成内存泄漏
}
V8引擎的垃圾回收
V8引擎采用分代垃圾回收策略,将内存分为新生代和老生代。
新生代(Young Generation)
- 存放生存时间短的对象
- 使用Scavenge算法(复制算法)
- 空间较小,垃圾回收频繁
老生代(Old Generation)
- 存放生存时间长的对象
- 使用标记清除和标记整理算法
- 空间大,垃圾回收不频繁
深入理解
内存分配
// 基本类型:存储在栈中
let num = 10;
let str = 'hello';
// 引用类型:对象存储在堆中,引用存储在栈中
let obj = { name: 'test' };
let arr = [1, 2, 3];
内存泄漏的常见情况
// 1. 意外的全局变量
function leak() {
globalVar = 'I am global'; // 没有声明,变成全局变量
}
// 2. 闭包
function outer() {
let largeData = new Array(1000000).fill('x');
return function inner() {
// 即使只使用了smallData,largeData也不会被回收
let smallData = largeData[0];
return smallData;
};
}
let leak = outer();
// 需要手动解除引用
leak = null;
// 3. 定时器
let data = { value: 'important' };
setInterval(() => {
console.log(data.value);
}, 1000);
// 如果不再需要data,但定时器还在运行,data不会被回收
// 4. DOM引用
let elements = {
button: document.getElementById('button')
};
// 即使从DOM中移除了button,elements.button仍然引用着它
优化垃圾回收
// 1. 及时解除引用
function processData() {
let largeArray = new Array(1000000);
// 处理数据...
// 处理完成后,解除引用
largeArray = null;
}
// 2. 使用对象池
class ObjectPool {
constructor(factory, size = 10) {
this.factory = factory;
this.pool = [];
for (let i = 0; i < size; i++) {
this.pool.push(factory());
}
}
acquire() {
return this.pool.pop() || this.factory();
}
release(obj) {
// 重置对象状态
Object.keys(obj).forEach(key => delete obj[key]);
this.pool.push(obj);
}
}
// 3. 避免频繁的内存分配
// 错误:每次循环都创建新数组
function bad() {
for (let i = 0; i < 1000; i++) {
let arr = new Array(1000); // 频繁分配内存
// 使用arr
}
}
// 正确:复用数组
function good() {
let arr = new Array(1000);
for (let i = 0; i < 1000; i++) {
// 复用arr,清空即可
arr.length = 0;
// 使用arr
}
}
最佳实践
- 及时解除引用:不再使用的对象,手动设置为null
- 避免全局变量:使用严格模式防止意外创建全局变量
- 管理事件监听:移除DOM元素前,先移除事件监听
- 注意闭包使用:避免闭包引用不必要的大对象
- 使用WeakMap/WeakSet:对于临时关联的数据,使用弱引用
// WeakMap的键是弱引用,不会阻止垃圾回收
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, 'some data');
// obj可以被垃圾回收,即使它还在WeakMap中
obj = null;
// WeakMap中的条目会自动被移除
面试要点
- 垃圾回收算法:标记清除和引用计数的原理及优缺点
- V8分代回收:新生代和老生代的不同策略
- 内存泄漏:能够识别常见的内存泄漏场景
- 性能优化:了解如何减少垃圾回收的压力
- WeakMap/WeakSet:理解弱引用的概念和应用场景
常见问题
Q:为什么需要垃圾回收? A:手动管理内存容易出错(忘记释放或重复释放),垃圾回收自动管理内存,减少程序员负担。
Q:标记清除和引用计数有什么区别? A:标记清除通过可达性分析判断对象是否存活,引用计数通过维护引用次数。引用计数无法处理循环引用问题。
Q:如何排查内存泄漏? A:使用Chrome DevTools的Memory面板,通过Heap Snapshot对比内存使用情况,找出持续增长的对象。