精读《JavaScript 高级程序设计 第4版》第四章(垃圾回收)
4.3 垃圾回收
垃圾回收(Garbage Collection,简称 GC)是 JavaScript 引擎自动管理内存的机制,它会定期找出不再使用的变量和对象,并释放它们占用的内存空间。在 JavaScript 中,开发者不需要手动分配和释放内存,这避免了:内存泄漏、野指针、重复释放、忘记释放内存等问题。
4.3.1 主要垃圾回收算法
1|引用计数(早期算法)
工作原理:跟踪每个值被引用的次数
// 引用计数示例
let obj1 = { name: "John" }; // 引用计数: 1
let obj2 = obj1; // 引用计数: 2
obj1 = null; // 引用计数: 1
obj2 = null; // 引用计数: 0 → 可回收
循环引用问题:
function problem() {
let objA = {};
let objB = {};
objA.ref = objB; // objB 被 objA 引用
objB.ref = objA; // objA 被 objB 引用
// 即使函数执行完毕,引用计数都不为0,无法回收
}
2、标记-清除算法(现代主流)
工作原理:从根对象(全局对象)开始,标记所有可达对象,清除未标记对象
// 标记-清除示例
function createObjects() {
let tempObj = { data: "temporary" };
let persistentObj = { data: "persistent" };
window.globalRef = persistentObj; // 被全局引用
// tempObj 在函数结束后不可达,将被回收
}
createObjects();
标记过程:
- 从根对象(window/global)开始
- 标记所有可访问的对象
- 遍历被标记对象的引用,递归标记
- 清除所有未标记的对象
4.3.2 内存管理
出于安全考虑,为了避免运行大量JavaScript的网页耗尽系统内存而导致操作系统崩溃,将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证执行代码时只保存必要的数据,如果数据不再需要,那么将它设置为null,从而释放其引用(解除引用)。这个方式适合全局变量和全局对象的属性,而局部变量会在超出作用域时自动解除引用。
1、通过const和let声明提升性能
ES6增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为const和let都以块(而非函数)为作用域,所以相比var,使用这两个关键字肯可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。
2、内存泄露
JavaScript中的内存泄露大部分是由不合理的引用导致的。常包含下列产生场景:
1 意外的全局变量
// ❌ 错误做法
function leak() {
leakedVar = '这是一个全局变量'; // 未使用 var/let/const
this.globalProp = '这也是全局变量';
}
// ✅ 正确做法
function noLeak() {
let localVar = '这是局部变量';
const localConst = '这也是局部变量';
}
2 闭包使用不当
// ❌ 可能造成内存泄漏
function createClosure() {
let largeData = new Array(1000000);
return function() {
// 即使不需要 largeData,它仍然被保留
console.log('一些操作');
};
}
// ✅ 优化版本
function createOptimizedClosure() {
let largeData = new Array(1000000);
// 使用完成后显式释放
const result = function() {
console.log('一些操作');
};
largeData = null; // 帮助 GC
return result;
}
3 定时器和回调函数
// ❌ 未清理的定时器
let data = { content: '大量数据' };
setInterval(() => {
console.log(data.content);
}, 1000);
// ✅ 正确管理
let timer;
function startProcess() {
let localData = { content: '数据' };
timer = setInterval(() => {
console.log(localData.content);
}, 1000);
}
function stopProcess() {
clearInterval(timer);
// localData 可以被回收
}
4 DOM 引用
// ❌ DOM 引用未清理
let elements = {
button: document.getElementById('myButton'),
container: document.getElementById('container')
};
// 即使从 DOM 中移除,JavaScript 引用仍然存在
document.body.removeChild(elements.container);
// ✅ 正确做法
function cleanUp() {
elements.button = null;
elements.container = null;
elements = null;
}
5 Event Listeners
// ❌ 未移除的事件监听器
class Component {
constructor() {
this.data = new Array(10000);
window.addEventListener('resize', this.handleResize.bind(this));
}
handleResize() {
console.log('窗口大小改变');
}
}
// ✅ 正确管理
class OptimizedComponent {
constructor() {
this.data = new Array(10000);
this.boundHandleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.boundHandleResize);
}
handleResize() {
console.log('窗口大小改变');
}
destroy() {
window.removeEventListener('resize', this.boundHandleResize);
this.data = null;
}
}
3、内存管理最佳实践
1 对象池模式
尤其是在移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。浏览器决定何时运行垃圾回收程序的一个标准就是对象更替速度。如果有很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采取更激进的方式,更频繁地调度垃圾回收程序运行,影响性能。解决办法可以在初始化的一刻,创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象,设置其属性、使用它,然后操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替。
class ObjectPool {
constructor(createFn) {
this.createFn = createFn;
this.pool = [];
}
get() {
return this.pool.length > 0 ? this.pool.pop() : this.createFn();
}
release(obj) {
// 重置对象状态
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
obj[key] = null;
}
});
this.pool.push(obj);
}
}
// 使用示例
const vectorPool = new ObjectPool(() => ({ x: 0, y: 0, z: 0 }));
const v1 = vectorPool.get();
v1.x = 10; v1.y = 20; v1.z = 30;
// 使用完成后...
vectorPool.release(v1);
2 避免内存泄漏的模式
// 使用 WeakMap 和 WeakSet 避免强引用
const weakMap = new WeakMap();
let domElement = document.getElementById('element');
weakMap.set(domElement, { someData: 'data' });
// 当 domElement 被移除时,关联数据会自动被 GC
domElement = null;
// 使用 WeakRef 和 FinalizationRegistry
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} 被垃圾回收了`);
});
let largeObject = { data: new Array(1000000) };
registry.register(largeObject, "大型对象");
largeObject = null; // 最终会被 GC 回收更多推荐



所有评论(0)