哪些情况会导致内存泄漏

5 阅读5分钟

以下是导致 JavaScript 内存泄漏的常见情况及具体示例:

一、全局变量引起的内存泄漏

1. 意外的全局变量

// 情况1:忘记声明变量
function createLeak() {
    leak = 'I am a global variable'; // 没有var/let/const
}

// 情况2:在非严格模式下,this指向window
function badConstructor() {
    this.property = 'I become global'; // this === window
}
badConstructor();

// 情况3:构造函数忘记使用new
function Person(name) {
    this.name = name;
}
// 忘记new,this指向全局对象
const p = Person('John'); // 泄露:window.name = 'John'

2. 被遗忘的定时器和回调函数

// 情况1:未清理的setInterval
let data = getLargeData();
const intervalId = setInterval(() => {
    // data一直存在,因为闭包引用
    console.log(data.length);
}, 1000);

// 忘记清理
// clearInterval(intervalId); // 缺少这行

// 情况2:未清理的事件监听器
class Component {
    constructor() {
        this.data = new Array(1000000).fill('*');
        this.handleClick = this.handleClick.bind(this);
        document.addEventListener('click', this.handleClick);
    }
    
    handleClick() {
        console.log('Clicked');
    }
    
    // 忘记在销毁时移除监听器
    // destroy() {
    //     document.removeEventListener('click', this.handleClick);
    // }
}

const comp = new Component();
// comp.destroy(); // 如果没有调用,data和handleClick都不会被释放

二、DOM 相关内存泄漏

1. 脱离 DOM 的引用

// 情况1:在JS中保留DOM引用,即使DOM已被移除
const elements = {
    button: document.getElementById('myButton'),
    image: document.getElementById('myImage')
};

// 从DOM中移除
document.body.removeChild(document.getElementById('myButton'));

// 但elements.button仍然引用着DOM元素
// 垃圾回收器无法回收这个button
// 应该:elements.button = null;

// 情况2:表格单元格引用问题
function createTable() {
    const table = document.createElement('table');
    const row = table.insertRow();
    const cell = row.insertCell();
    cell.textContent = 'Data';
    
    // 某些浏览器中,单元格引用整行
    const cellRef = cell; // 保留单元格引用
    
    // 即使从DOM中移除表格
    document.body.removeChild(table);
    
    // cellRef仍然间接引用整个表格
    return cellRef; // 导致整个表格无法被回收
}

2. 闭包陷阱

// 情况1:闭包持有大对象
function outer() {
    const hugeArray = new Array(1000000).fill('*');
    const hugeObject = { data: new Array(500000).fill('*') };
    
    return function inner() {
        // inner函数闭包引用了hugeArray和hugeObject
        console.log('Hello');
        // 即使inner不直接使用hugeArray,它仍然被保留
    };
}

const closure = outer(); // hugeArray和hugeObject不会被释放

// 情况2:事件处理器中的闭包
function attachEvents() {
    const element = document.getElementById('myElement');
    const bigData = getHugeData();
    
    element.addEventListener('click', function() {
        // 这个匿名函数闭包引用了bigData
        console.log(bigData.length);
    });
    
    // 即使移除元素,事件处理器中的闭包仍然持有bigData
    // document.body.removeChild(element);
}

三、缓存不当导致的内存泄漏

// 情况1:无限增长的缓存
const cache = {};

function processData(key) {
    if (!cache[key]) {
        // 没有大小限制,缓存会无限增长
        cache[key] = expensiveOperation(key);
    }
    return cache[key];
}

// 情况2:忘记清理的Map/Set
const activeRequests = new Map();

function makeRequest(url) {
    const controller = new AbortController();
    activeRequests.set(url, controller);
    
    fetch(url, { signal: controller.signal })
        .then(response => {
            // 处理响应后忘记清理
            // activeRequests.delete(url); // 缺少这行
        })
        .catch(() => {
            // activeRequests.delete(url); // 缺少这行
        });
}

// 情况3:对象属性作为缓存
function DataProcessor() {
    this.cache = {}; // 实例属性作为缓存
}

DataProcessor.prototype.process = function(key) {
    if (!this.cache[key]) {
        this.cache[key] = computeExpensiveValue(key);
    }
    return this.cache[key];
};

// 创建多个实例,每个都有缓存
const processors = Array(1000).fill().map(() => new DataProcessor());

四、循环引用问题

// 情况1:DOM与JS对象的循环引用
function createCircularReference() {
    const element = document.getElementById('someElement');
    const obj = {};
    
    obj.element = element;     // JS对象引用DOM
    element.objRef = obj;      // DOM引用JS对象
    
    // 即使从DOM中移除元素
    document.body.removeChild(element);
    
    // 在某些浏览器(特别是旧版IE)中,循环引用会导致内存泄漏
    // 现代浏览器通常能处理,但最好手动解除
    // element.objRef = null;
}

// 情况2:对象间的循环引用
function createObjectCycle() {
    const objA = { name: 'A' };
    const objB = { name: 'B' };
    
    objA.ref = objB;  // A引用B
    objB.ref = objA;  // B引用A
    
    // 如果objA和objB不再被其他代码引用,现代GC能回收
    // 但如果有外部引用其中一个,两者都无法回收
    return objA; // 只返回objA,但objB也无法被回收
}

五、被遗忘的订阅/观察者

// 情况1:Pub/Sub模式中的订阅
class EventBus {
    constructor() {
        this.subscribers = [];
    }
    
    subscribe(callback) {
        this.subscribers.push(callback);
        return () => {
            const index = this.subscribers.indexOf(callback);
            if (index > -1) this.subscribers.splice(index, 1);
        };
    }
}

const bus = new EventBus();
const bigData = new Array(1000000).fill('data');

// 订阅
const unsubscribe = bus.subscribe(() => {
    console.log(bigData.length); // 闭包引用bigData
});

// 忘记取消订阅
// 组件销毁时应该调用:unsubscribe();

// 情况2:ResizeObserver/MutationObserver
function setupObserver() {
    const element = document.getElementById('observed');
    const data = getLargeData();
    
    const observer = new ResizeObserver((entries) => {
        // 闭包引用data
        console.log(data.length, element.offsetWidth);
    });
    
    observer.observe(element);
    
    // 元素被移除时忘记disconnect
    // element.parentNode.removeChild(element);
    // observer.disconnect(); // 缺少这行
}

六、Web API 相关泄漏

// 情况1:未关闭的WebSocket
let socket = new WebSocket('ws://example.com');
socket.onmessage = function(event) {
    const largeData = JSON.parse(event.data);
    processData(largeData);
};

// 页面离开时忘记关闭
// window.addEventListener('beforeunload', () => {
//     socket.close(); // 缺少这行
// });

// 情况2:未释放的Web Workers
const worker = new Worker('worker.js');
worker.postMessage({ command: 'start', data: largeArrayBuffer });

// 忘记终止worker
// worker.terminate(); // 缺少这行

// 情况3:未清除的FileReader
const reader = new FileReader();
reader.onload = function(e) {
    const largeText = e.target.result;
    // 处理大文件内容
};
reader.readAsText(largeFile);

// reader对象会一直存在,直到被覆盖或页面卸载

七、框架特定的内存泄漏

React 示例:

// 情况1:未清理的副作用
import { useEffect, useState } from 'react';

function LeakyComponent() {
    const [data, setData] = useState(null);
    
    useEffect(() => {
        // 异步操作,组件卸载后可能还在执行
        fetchData().then(result => {
            setData(result); // 组件卸载后设置状态
        });
        
        // 缺少清理函数
        // return () => {
        //     // 取消请求或设置标志位
        // };
    }, []);
    
    return <div>{data}</div>;
}

// 情况2:事件监听器未移除
function EventListenerLeak() {
    useEffect(() => {
        const handleResize = () => {
            console.log(window.innerWidth);
        };
        
        window.addEventListener('resize', handleResize);
        
        // 忘记移除
        // return () => window.removeEventListener('resize', handleResize);
    }, []);
    
    return <div>Content</div>;
}

Vue 示例:

// 情况1:全局事件总线未清理
export default {
    mounted() {
        // 全局事件监听
        EventBus.$on('some-event', this.handleEvent);
    },
    beforeDestroy() {
        // 忘记移除
        // EventBus.$off('some-event', this.handleEvent);
    }
};

// 情况2:第三方库实例未清理
export default {
    data() {
        return {
            chart: null
        };
    },
    mounted() {
        this.chart = new Chart(this.$refs.canvas, {
            // 大量配置和数据
        });
    },
    beforeDestroy() {
        // 忘记销毁
        // this.chart.destroy();
        // this.chart = null;
    }
};

八、Console 引起的泄漏

// 开发中常见但易忽略的问题
function processUserData(user) {
    const processed = expensiveProcessing(user);
    
    // ❌ 调试代码留在生产环境
    console.log(processed); // 控制台保持对象引用
    
    // ❌ 将大对象记录到控制台
    // console.log(largeArray); // 控制台可能保留完整快照
    
    return processed;
}

// 某些情况下,控制台会阻止垃圾回收
const hugeObject = { data: new Array(1000000).fill('x') };
console.log('Object:', hugeObject); // 控制台持有引用
hugeObject = null; // 但控制台仍然可以访问它

九、检测和预防内存泄漏的工具

1. Chrome DevTools

  • Memory面板:拍摄堆快照,比较前后差异
  • Performance面板:记录内存分配时间线
  • Allocation instrumentation:跟踪内存分配

2. 代码检查工具

// 使用WeakMap/WeakSet避免强引用
const weakCache = new WeakMap(); // 键是弱引用

// 使用WeakRef和FinalizationRegistry
const registry = new FinalizationRegistry((heldValue) => {
    console.log(`${heldValue} 被回收了`);
});

const obj = { data: 'large' };
const weakRef = new WeakRef(obj);
registry.register(obj, 'myObject');

// 当obj被回收时,会触发回调

3. 监控代码

// 定期检查内存使用
function monitorMemory() {
    if (window.performance && performance.memory) {
        const used = performance.memory.usedJSHeapSize;
        const limit = performance.memory.jsHeapSizeLimit;
        const percent = (used / limit * 100).toFixed(2);
        
        if (percent > 80) {
            console.warn(`内存使用率过高: ${percent}%`);
        }
    }
}

setInterval(monitorMemory, 30000);

十、最佳实践总结

  1. 及时清理引用:不再使用的对象设为 null
  2. 使用弱引用WeakMapWeakSetWeakRef
  3. 清理事件监听器removeEventListenerdisconnect
  4. 清理定时器clearIntervalclearTimeout
  5. 注意闭包:避免不必要的大对象被闭包捕获
  6. 框架生命周期:在组件销毁时清理资源
  7. 避免全局缓存:为缓存设置大小限制和过期策略
  8. 生产环境移除console:使用构建工具自动移除
  9. 代码审查:定期检查常见的内存泄漏模式
  10. 性能测试:使用自动化测试检测内存泄漏

内存泄漏通常不会立即显现,但会随着时间积累导致应用变慢或崩溃。通过理解这些常见模式并采用预防措施,可以显著减少内存泄漏问题。