内存泄漏检测与优化
内存泄漏会导致页面卡顿甚至崩溃,掌握检测与修复方法是高级开发必备技能。
JavaScript内存管理
内存生命周期
JavaScript
分配 → 使用 → 释放
垃圾回收机制
JavaScript
// 标记清除算法
// 1. 标记所有可达对象
// 2. 清除不可达对象
//可达性示例
let global = { data: 'reachable' }; // 全局变量始终可达
function createLocal() {
let local = { data: 'local' }; // 函数结束后不可达
return local; // 返回后保持可达
}
// 引用链
const a = {};
const b = { ref: a }; // a通过b可达
b.ref = null; // a不再可达,等待回收
V8内存结构
JavaScript
堆内存分区:
- New Space:新生代(短生命周期对象)
- Old Space:老生代(长生命周期对象)
- Large Object Space:大对象区
- Code Space:代码区
常见泄漏场景
全局变量泄漏
JavaScript
// ❌ 未声明的全局变量
function createGlobal() {
leaked = 'global variable'; // 沏漏到window
}
// ❌ 全局变量累积
window.dataList = [];
function addItem(item) {
window.dataList.push(item); // 持续增长不清理
}
// ✅ 使用局部变量或及时清理
function createLocal() {
const local = 'local variable'; // 函数结束回收
return local;
}
// ✅ 全局变量定期清理
window.cache = new Map();
setInterval(() => {
window.cache.clear();
}, 60000);
闭包泄漏
JavaScript
// ❌ 闭包引用大对象
function createClosure() {
const largeData = new Array(1000000);
return function() {
return largeData.length; // largeData被闭包持有
};
}
const fn = createClosure(); // largeData不会被回收
// ✅ 只保留必要数据
function createClosure() {
const largeData = new Array(1000000);
const length = largeData.length; // 只保存需要的数据
return function() {
return length; // largeData可被回收
};
}
DOM引用泄漏
JavaScript
// ❌ 保存DOM引用但DOM已移除
const elements = [];
function addElement() {
const el = document.createElement('div');
document.body.appendChild(el);
elements.push(el); // 保存引用
}
function removeElement() {
const el = elements.pop();
document.body.removeChild(el); // DOM移除但引用保留
}
// ✅ 移除DOM时清除引用
function removeElement() {
const el = elements.pop();
document.body.removeChild(el);
elements.length = 0; // 清除引用
}
// ✅ 使用WeakMap避免强引用
const weakElements = new WeakMap();
function addElement() {
const el = document.createElement('div');
document.body.appendChild(el);
weakElements.set(el, { data: 'metadata' });
}
// DOM移除后自动清除WeakMap引用
事件监听泄漏
JavaScript
// ❌ 未移除事件监听
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
document.addEventListener('click', this.handleClick);
}
destroy() {
// 忘记移除监听器
}
}
// ✅ 销毁时移除监听
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
document.addEventListener('click', this.handleClick);
}
destroy() {
document.removeEventListener('click', this.handleClick);
}
}
// ✅ 使用AbortController(现代方式)
class Component {
constructor() {
this.abortController = new AbortController();
document.addEventListener('click', this.handleClick, {
signal: this.abortController.signal
});
}
destroy() {
this.abortController.abort(); // 自动移除所有监听
}
}
定时器泄漏
JavaScript
// ❌ 未清除定时器
class TimerComponent {
constructor() {
this.timer = setInterval(() => {
this.update();
}, 1000);
}
destroy() {
// 未清除interval
}
}
// ✅ 销毁时清除定时器
class TimerComponent {
constructor() {
this.timer = setInterval(() => this.update(), 1000);
}
destroy() {
clearInterval(this.timer);
}
}
// ✅ 使用WeakRef自动清理
class AutoTimer {
constructor(callback, interval) {
this.timer = setInterval(() => {
if (this.weakRef.deref()) {
callback();
} else {
clearInterval(this.timer); // 对象不存在自动清理
}
}, interval);
}
}
闭包循环引用
JavaScript
// ❌ 闭包持有外部引用
function leakExample() {
const obj = { fn: null };
obj.fn = function() {
return obj; // 循环引用:obj.fn → obj
};
return obj;
}
// ✅ 打破循环引用
function fixedExample() {
const obj = { fn: null };
obj.fn = function() {
return this; // 使用this而非obj
};
return obj;
}
// ✅ 使用WeakMap打破循环
const weakMap = new WeakMap();
function useWeakMap(obj) {
weakMap.set(obj, () => obj);
}
内存泄漏检测
Chrome DevTools Memory
JavaScript
检测流程:
1. DevTools → Memory → Take heap snapshot
2. 执行可能泄漏的操作
3. 再次Take heap snapshot
4. 对比两个快照,查看增量对象
关键指标:
- Detached DOM nodes:脱离DOM树但仍有引用
- Object retained size:对象保留大小
- Comparison视图:对比快照找出增长对象
Allocation Timeline
JavaScript
使用方法:
1. DevTools → Memory → Allocation timeline
2. 录制内存分配过程
3. 执行可疑操作
4. 分析分配峰值,查看哪些对象持续增长
堆快照对比
JavaScript
// 识别泄漏对象
// 1. 打开Comparison视图
// 2. 关注New列(新增对象)
// 3. 关注Delta列(大小变化)
// 4. 定位retain chain(引用链)
Console检测
JavaScript
// 查看DOM节点数量
function countDOMNodes() {
console.log('Total nodes:', document.getElementsByTagName('*').length);
}
// 查看分离节点
// DevTools Console执行:
// queryObjects(Document) // 查看所有Document对象
// 内存使用监控
function checkMemory() {
console.log('Used JS heap size:', performance.memory.usedJSHeapSize);
console.log('Total JS heap size:', performance.memory.totalJSHeapSize);
}
// 定期监控
setInterval(checkMemory, 5000);
WeakMap检测
JavaScript
// 使用WeakMap检测对象是否被回收
const weakRefs = new WeakMap();
function trackObject(obj, id) {
weakRefs.set(obj, id);
}
function checkCollected() {
// 检查WeakMap中的对象是否还存在
for (const [obj, id] of weakRefs) {
console.log(`Object ${id} still exists`);
}
}
// 配合FinalizationRegistry
const registry = new FinalizationRegistry((id) => {
console.log(`Object ${id} has been collected`);
});
function trackWithFinalization(obj, id) {
registry.register(obj, id);
}
// 对象回收时会触发回调
内存优化策略
及时释放引用
JavaScript
// 组件销毁模式
class Component {
constructor() {
this.data = {};
this.listeners = [];
this.timer = null;
}
destroy() {
// 清除所有引用
this.data = null;
this.listeners.forEach(l => l.remove());
this.listeners = [];
clearInterval(this.timer);
}
}
使用弱引用
JavaScript
// WeakMap:键是弱引用
const cache = new WeakMap();
cache.set(object, metadata);
// object被回收时,entry自动清除
// WeakSet:元素是弱引用
const tracked = new WeakSet();
tracked.add(object);
// object被回收时自动移除
// WeakRef:持有弱引用
const ref = new WeakRef(object);
const obj = ref.deref(); // 获取对象,可能为undefined
对象池模式
text
// 复用对象减少创建
class ObjectPool {
constructor(factory, initialSize = 10) {
this.factory = factory;
this.pool = [];
for (let i = 0; i < initialSize; i++) {
this.pool.push(factory());
}
}
acquire() {
return this.pool.length > 0
? this.pool.pop()
: this.factory();
}
release(obj) {
// 重置对象状态
this.reset(obj);
this.pool.push(obj);
}
reset(obj) {
// 清除对象数据
for (const key in obj) {
obj[key] = null;
}
}
}
// 使用
const pool = new ObjectPool(() => ({ data: null }));
const obj = pool.acquire();
obj.data = 'use';
pool.release(obj);
避免过度缓存
text
// ❌ 无限缓存
const cache = new Map();
function addToCache(key, value) {
cache.set(key, value); // 无限制增长
}
// ✅ 限制缓存大小
class LimitedCache {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
// ✅ 带过期缓存
class TTLCache {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
set(key, value) {
this.cache.set(key, { value, expires: Date.now() + this.ttl });
}
get(key) {
const item = this.cache.get(key);
if (!item || Date.now() > item.expires) {
this.cache.delete(key);
return null;
}
return item.value;
}
}
最佳实践
组件生命周期管理
text
// React组件清理
useEffect(() => {
const timer = setInterval(callback, 1000);
const handler = () => {};
document.addEventListener('click', handler);
return () => {
clearInterval(timer);
document.removeEventListener('click', handler);
};
}, []);
// Vue组件清理
onUnmounted(() => {
clearInterval(timer);
eventBus.off('event', handler);
});
大数据处理
text
// ❌ 一次性处理全部数据
function processLargeData(data) {
return data.map(item => transform(item)); // 内存峰值
}
// ✅ 分批处理
async function processInBatches(data, batchSize = 1000) {
const results = [];
for (let i = 0; i < data.length; i += batchSize) {
const batch = data.slice(i, i + batchSize);
results.push(...batch.map(transform));
// 让GC有机会回收
await new Promise(r => setTimeout(r, 0));
}
return results;
}
要点总结
- GC原理:标记清除算法,可达性决定回收
- 常见泄漏:全局变量、闭包、DOM引用、事件监听、定时器
- 检测工具:DevTools Memory、堆快照对比、Allocation timeline
- 弱引用:WeakMap/WeakSet/WeakRef打破强引用链
- 生命周期:组件销毁必须清理所有引用和监听
- 对象池:复用对象减少GC压力
存放路径:articles/JS/专家/高级性能分析/内存泄漏检测与优化.md
📝 发现内容有误?点击此处直接编辑