一、什么是内存泄漏?
内存泄漏就像是你租了一间房子,合同到期后却忘记退租,房东也没发现,结果你一直白白交着房租。在JavaScript中,内存泄漏指的是程序不再需要的内存没有被垃圾回收机制(GC)释放,导致可用内存越来越少,最终可能引发页面卡顿甚至崩溃。
举个简单的例子:
// 技术栈:JavaScript(浏览器环境)
function createLeak() {
const hugeArray = new Array(1000000).fill('leak'); // 创建一个超大数组
document.getElementById('leakBtn').addEventListener('click', () => {
console.log(hugeArray.length); // 闭包引用了hugeArray,导致无法释放
});
}
createLeak();
// 即使createLeak执行完毕,hugeArray仍被事件监听器引用,无法被GC回收
这里,hugeArray被事件监听器的回调函数引用,即使createLeak函数执行完毕,内存也无法释放。
二、常见的内存泄漏模式
1. 意外的全局变量
JavaScript中,如果忘记声明变量(比如写错let或const),变量会变成全局的,直到页面关闭才会释放。
// 技术栈:JavaScript
function leakGlobal() {
leak = 'I am a global variable!'; // 本意是局部变量,但漏了var/let/const
}
leakGlobal();
// 此时window.leak存在,直到页面关闭
解决方法:严格模式('use strict')会直接报错,避免这种问题。
2. 被遗忘的定时器和回调
定时器(setInterval/setTimeout)和事件监听器如果不清除,会一直持有引用。
// 技术栈:Node.js(setInterval示例)
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
function startLeakyTimer() {
setInterval(() => {
emitter.emit('ping'); // 定时器持续运行,即使不再需要
}, 1000);
}
startLeakyTimer();
// 正确的做法是保存timerId,并在适当时clearInterval
3. DOM引用未清理
手动保存的DOM元素引用,即使节点从页面移除,只要引用存在,内存就不会释放。
// 技术栈:浏览器JavaScript
const elements = {
button: document.getElementById('oldButton'), // 保存DOM引用
};
document.body.removeChild(document.getElementById('oldButton')); // 从DOM移除
// 但elements.button仍引用该节点,内存无法回收
4. 闭包滥用
闭包是JavaScript的强大特性,但过度使用会导致变量长期驻留内存。
// 技术栈:JavaScript
function createClosureLeak() {
const data = 'Sensitive Data'; // 本应短期存在的变量
return function() {
console.log(data); // 闭包导致data无法释放
};
}
const leakedFn = createClosureLeak();
// 即使不再需要leakedFn,data仍被持有
三、排查内存泄漏的技巧
1. 使用开发者工具
Chrome DevTools的Memory面板是神器:
- 拍下堆快照(Heap Snapshot)。
- 执行可疑操作。
- 再拍快照,对比前后变化,找到未被释放的对象。
2. 监控内存变化
通过performance.memory(浏览器)或process.memoryUsage()(Node.js)实时观察:
// 技术栈:Node.js
setInterval(() => {
const memory = process.memoryUsage();
console.log(`RSS: ${memory.rss / 1024 / 1024} MB`); // 常驻内存
}, 1000);
3. 代码审查
重点关注:
- 全局变量。
- 未清理的定时器/事件监听器。
- 大型数据结构(如缓存未设置上限)。
四、实战:修复一个真实案例
假设我们有一个单页应用(SPA),用户反馈切换页面后越来越卡。
问题代码:
// 技术栈:Vue.js(但问题通用)
export default {
mounted() {
window.addEventListener('resize', this.handleResize); // 添加监听
},
methods: {
handleResize() {
// 处理逻辑
},
},
// 忘记在beforeDestroy中移除监听!
};
修复方案:
export default {
mounted() {
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize); // 清理监听
},
methods: {
handleResize() {
// 处理逻辑
},
},
};
五、总结
内存泄漏的核心原因是“该释放的没释放”。预防的关键在于:
- 避免隐式全局变量。
- 及时清理定时器、事件监听器和DOM引用。
- 合理使用闭包。
- 善用开发者工具排查。
在大型应用中,内存问题可能不会立刻暴露,但随着时间推移,小漏洞会累积成大问题。养成良好的编码习惯,定期进行内存检查,才能让应用长期稳定运行。
评论