深入了解 JavaScript 内存泄漏
字数 1296 2025-08-11 08:36:07
JavaScript 内存泄漏全面解析与防治指南
一、内存基础概念
1.1 什么是内存
在硬件级别上,计算机内存由大量触发器组成,每个触发器能够存储一个位。从概念上讲,整个计算机内存可以看作是一个巨大的位数组,可以进行读写操作。
JavaScript 作为高级语言,通过引擎自动管理内存读写,开发者无需直接操作二进制内存。
1.2 内存生命周期
内存生命周期分为三个阶段:
- 分配期:分配所需内存
- 使用期:使用分配的内存进行读写操作
- 释放期:不再需要时释放内存
流程:内存分配 → 内存使用 → 内存释放
二、内存泄漏定义
内存泄漏指由于疏忽或错误导致程序未能释放不再使用的内存。并非物理消失,而是应用程序在释放前失去了对该内存的控制,造成内存浪费。
简单理解:无用的内存仍在占用,得不到释放。严重时会导致系统卡顿甚至崩溃。
三、JavaScript 内存管理机制
3.1 内存分配
JavaScript 自动分配内存:
let num = 1;
const str = "名字";
const obj = { a: 1, b: 2 };
const arr = [1, 2, 3];
function func(arg) { /*...*/ }
3.2 内存使用
对分配的内存进行读写操作:
num = 2; // 写入
func(num); // 读取并传递
3.3 内存回收(垃圾回收 GC)
JavaScript 使用两种主要垃圾回收算法:
1. 引用计数法
- 定义:通过计数对象的引用次数判断是否回收
- 问题:循环引用时无法回收
- 手动回收示例:
var obj1 = { a: 1 };
var obj2 = obj1;
obj1 = 1; // 对象仍有引用
obj2 = null; // 手动释放
2. 标记清除法
- 定义:通过"进入环境"和"离开环境"标记判断
- 流程:
- 变量进入执行环境时标记"进入环境"
- 变量离开执行环境时标记"离开环境"
- 回收被标记为"离开环境"的变量
var b = 1; // 全局,页面关闭才销毁
function func() {
var a = 1; // 函数执行时标记"进入环境"
return a + b;
} // 函数结束,a标记"离开环境"被回收
四、常见内存泄漏场景及解决方案
4.1 意外的全局变量
function count(num) {
a = 1; // 意外创建全局变量
return a + num;
}
解决:使用严格模式或ESLint检查
4.2 遗忘的计时器
// Vue组件示例
mounted() {
setInterval(this.fetchData, 2000); // 组件销毁后仍运行
}
解决:
mounted() {
this.timer = setInterval(this.fetchData, 2000);
},
beforeDestroy() {
clearInterval(this.timer);
}
4.3 遗忘的事件监听
mounted() {
window.addEventListener('resize', this.resizeHandler);
}
解决:
mounted() {
window.addEventListener('resize', this.resizeHandler);
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeHandler);
}
4.4 遗忘的Set/Map结构
Set内存泄漏:
let set = new Set();
let value = { a: 1 };
set.add(value);
value = null; // 内存泄漏
解决:
set.delete(value); // 手动删除
value = null;
// 或使用WeakSet
let weakSet = new WeakSet();
weakSet.add(value);
value = null; // 自动回收
Map内存泄漏:
let map = new Map();
let key = [1, 2, 3];
map.set(key, 1);
key = null; // 内存泄漏
解决:
map.delete(key); // 手动删除
key = null;
// 或使用WeakMap
let weakMap = new WeakMap();
weakMap.set(key, 1);
key = null; // 自动回收
4.5 遗忘的订阅发布
mounted() {
EventEmitter.on('test', this.testHandler);
}
解决:
mounted() {
EventEmitter.on('test', this.testHandler);
},
beforeDestroy() {
EventEmitter.off('test', this.testHandler);
}
4.6 闭包引起的内存泄漏
function closure() {
const name = '名字';
return () => name.split('').reverse().join('');
}
const reverseName = closure(); // 即使不使用,name仍被引用
解决:谨慎使用闭包,确保必要性和可控性
4.7 DOM引用泄漏
class Test {
constructor() {
this.elements = {
button: document.querySelector('#button')
};
}
removeButton() {
document.body.removeChild(this.elements.button);
// 需要添加: this.elements.button = null;
}
}
解决:移除DOM后手动清除引用
五、内存泄漏检测方法
5.1 使用Chrome开发者工具
-
Performance工具:
- 勾选Memory选项
- 录制内存变化
- 观察内存走势图判断周期性增长
-
Memory工具:
- 多次录制堆内存快照
- 比较快照找出异常增长的对象
- 定位具体变量和代码位置
5.2 示例检测流程
<html>
<body>
<div id="app">
<button id="run">运行</button>
<button id="stop">停止</button>
</div>
<script>
const arr = [];
for (let i = 0; i < 200000; i++) arr.push(i);
let newArr = [];
function run() {
newArr = newArr.concat(arr); // 内存泄漏点
}
let clearRun;
document.querySelector('#run').onclick = function() {
clearRun = setInterval(run, 1000);
};
document.querySelector('#stop').onclick = function() {
clearInterval(clearRun);
};
</script>
</body>
</html>
检测步骤:
- 使用Performance工具录制内存变化
- 观察点击"运行"后内存周期性增长
- 点击"停止"后内存停止增长确认泄漏
- 使用Memory工具拍摄堆快照
- 分析发现array对象占用异常
- 定位到newArr变量及其相关代码
六、最佳实践总结
- 及时清理:计时器、事件监听、订阅发布等需要手动清理
- 弱引用:优先使用WeakSet/WeakMap处理临时引用
- DOM管理:移除DOM元素后清除相关引用
- 闭包谨慎:避免不必要的闭包长期持有引用
- 工具检测:定期使用开发者工具检测内存问题
- 代码规范:使用ESLint等工具避免意外全局变量
通过理解内存管理机制、识别常见泄漏场景并采用适当防治措施,可以有效避免JavaScript内存泄漏问题,提升应用性能。