您现在的位置是:网站首页 > 内存泄漏排查文章详情
内存泄漏排查
陈川
【
Node.js
】
26060人已围观
6784字
内存泄漏的基本概念
内存泄漏指的是程序中已分配的内存未能被正确释放,导致可用内存逐渐减少。在Node.js中,由于V8引擎的内存管理机制和事件循环的特性,内存泄漏问题可能表现得更为隐蔽。常见的内存泄漏场景包括未清理的定时器、闭包引用、全局变量滥用以及事件监听器未移除等。
常见的内存泄漏场景
未清理的定时器
function leakyFunction() {
setInterval(() => {
console.log('This interval keeps running...');
}, 1000);
}
leakyFunction();
这个例子中,setInterval
创建的定时器会持续运行,即使leakyFunction
已经执行完毕。如果没有显式调用clearInterval
,这个定时器会一直存在,导致内存泄漏。
闭包引用
function createClosure() {
const hugeArray = new Array(1000000).fill('*');
return function() {
console.log(hugeArray.length);
};
}
const closure = createClosure();
这里返回的闭包函数持有了对hugeArray
的引用,即使外部不再需要这个数组,它也无法被垃圾回收。
未移除的事件监听器
const EventEmitter = require('events');
const emitter = new EventEmitter();
function addListener() {
emitter.on('event', () => {
console.log('Event handled');
});
}
addListener();
// 之后没有调用 emitter.off('event')
每次调用addListener
都会添加一个新的事件监听器,如果不手动移除,这些监听器会一直存在于内存中。
内存泄漏的检测工具
Chrome DevTools
- 启动Node.js时添加
--inspect
标志:node --inspect your-script.js
- 打开Chrome浏览器,访问
chrome://inspect
- 点击"Open dedicated DevTools for Node"
- 在"Memory"标签页中可以进行堆内存快照
heapdump模块
const heapdump = require('heapdump');
// 手动触发堆内存转储
heapdump.writeSnapshot('/tmp/' + Date.now() + '.heapsnapshot');
Node.js内置的process.memoryUsage()
setInterval(() => {
const memoryUsage = process.memoryUsage();
console.log({
rss: memoryUsage.rss / 1024 / 1024 + 'MB',
heapTotal: memoryUsage.heapTotal / 1024 / 1024 + 'MB',
heapUsed: memoryUsage.heapUsed / 1024 / 1024 + 'MB',
external: memoryUsage.external / 1024 / 1024 + 'MB',
});
}, 1000);
分析内存快照
- 在Chrome DevTools中比较两个时间点的堆快照
- 关注"Retainers"面板,查看对象的引用链
- 查找意外保留的大对象
- 特别注意:
- Detached DOM树(在浏览器环境中)
- 闭包
- 缓存对象
- 全局变量
内存泄漏的修复策略
定时器清理
let intervalId;
function startInterval() {
intervalId = setInterval(() => {
console.log('Running...');
}, 1000);
}
function stopInterval() {
clearInterval(intervalId);
}
正确管理事件监听器
class MyComponent {
constructor() {
this.handleEvent = this.handleEvent.bind(this);
emitter.on('event', this.handleEvent);
}
handleEvent() {
console.log('Event received');
}
destroy() {
emitter.off('event', this.handleEvent);
}
}
避免全局变量
// 不好的做法
function storeData() {
global.cache = {};
}
// 更好的做法
class DataStore {
constructor() {
this.cache = {};
}
clear() {
this.cache = null;
}
}
流和缓冲区管理
const fs = require('fs');
function readFile() {
const stream = fs.createReadStream('large-file.txt');
let data = '';
stream.on('data', (chunk) => {
data += chunk; // 可能导致内存问题
});
// 更好的方式
const chunks = [];
stream.on('data', (chunk) => {
chunks.push(chunk);
});
stream.on('end', () => {
const buffer = Buffer.concat(chunks);
// 处理buffer
});
}
内存泄漏预防的最佳实践
-
使用
WeakMap
和WeakSet
存储临时引用const weakMap = new WeakMap(); let obj = {}; weakMap.set(obj, 'some data'); obj = null; // 当obj被垃圾回收时,WeakMap中的条目会自动移除
-
限制缓存大小并实现淘汰策略
class LRUCache { constructor(maxSize) { this.maxSize = maxSize; this.cache = new Map(); } get(key) { const value = this.cache.get(key); if (value) { this.cache.delete(key); this.cache.set(key, value); } return value; } 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); } }
-
使用内存监控中间件
function memoryMonitor(req, res, next) { const startMemory = process.memoryUsage(); res.on('finish', () => { const endMemory = process.memoryUsage(); const delta = endMemory.heapUsed - startMemory.heapUsed; console.log(`Memory delta: ${delta} bytes`); }); next(); } app.use(memoryMonitor);
-
定期压力测试和内存分析
const autocannon = require('autocannon'); const { promisify } = require('util'); const run = promisify(autocannon); async function stressTest() { await run({ url: 'http://localhost:3000', connections: 100, duration: 60 }); // 测试完成后立即进行堆快照 const heapdump = require('heapdump'); heapdump.writeSnapshot(); }
Node.js特定场景的内存管理
集群模式下的内存泄漏
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
// Worker代码
require('./server');
// 监控内存使用
setInterval(() => {
if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
process.exit(1); // 内存超过500MB时重启
}
}, 5000);
}
大JSON处理
// 不好的做法
const data = JSON.parse(fs.readFileSync('large.json', 'utf8'));
// 更好的做法
const stream = fs.createReadStream('large.json');
const jsonStream = require('JSONStream');
const parser = jsonStream.parse('*');
stream.pipe(parser)
.on('data', (item) => {
// 逐项处理
})
.on('end', () => {
console.log('Finished processing');
});
数据库连接泄漏
const { Pool } = require('pg');
const pool = new Pool({ max: 20 });
async function queryDatabase() {
let client;
try {
client = await pool.connect();
const result = await client.query('SELECT * FROM users');
return result.rows;
} finally {
if (client) client.release(); // 确保连接被释放
}
}
长期运行进程的特殊考虑
对于需要长期运行的Node.js进程(如服务器应用),还需要考虑:
- 内存碎片化问题
- V8引擎的内存限制(默认约1.4GB)
- 外部内存(如Buffer)不计入V8堆内存
- 使用
--max-old-space-size
调整内存限制node --max-old-space-size=4096 server.js
真实案例分析
一个Express应用的内存泄漏问题:
const express = require('express');
const app = express();
const requests = []; // 问题所在
app.get('/leaky', (req, res) => {
requests.push({
url: req.url,
date: new Date()
});
res.send('OK');
});
app.listen(3000);
问题在于requests
数组会无限增长。修复方案:
const express = require('express');
const app = express();
const requests = new Map();
const MAX_REQUESTS = 1000;
app.get('/fixed', (req, res) => {
if (requests.size >= MAX_REQUESTS) {
const oldestKey = requests.keys().next().value;
requests.delete(oldestKey);
}
requests.set(Date.now(), {
url: req.url,
date: new Date()
});
res.send('OK');
});