场景
一、定时器
1.1 setTimeout
setTimeout
没有被清理,导致引用持续存在,内存无法释放。需要使用 clearTimeout
清理定时器
1.2 setInterval
setInterval
没有被清理,导致引用持续存在,内存无法释放。需要使用 clearInterval
清理定时器
function leak() {
setInterval(() => {
console.log('Running');
}, 1000); // 定时器不会停止
}
leak();
const interval = setInterval(() => {
console.log('Running');
}, 1000);
// 在合适时机清理
clearInterval(interval);
二、闭包引用
闭包中对变量的引用被保留,导致内存无法被释放。避免不必要的闭包引用,确保及时释放大对象。
function createLeak() {
const bigData = new Array(1000000).fill('*');
return () => console.log(bigData.length); // 闭包引用了 bigData
}
const leak = createLeak();
三、数组引用
3.1 全局数组持续新增、插入
全局数组 全局数组持续通过 push
、append
等操作新增、插入数据,而由于存在强引用,这些对象无法被垃圾回收器(GC
)清理。所以导致,请求越多,那么 leaks
占用的内存越来越大。
const http = require('http');
const leaks = [];
const server = http.createServer((req, res) => {
leaks.push(new Array(1000000).fill('*')); // 内存泄漏
res.end('Memory Leak Test');
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
方案一: 限制数组大小: 限制数组的大小,例如只存储最近的请求数据。使用 leaks.shift()
删除数组中的最旧元素,确保 leaks
不会无限增长。防止内存泄漏。
const http = require('http');
const leaks = [];
const MAX_LEAKS_SIZE = 10; // 限制数组大小
const server = http.createServer((req, res) => {
// 超过限制后,移除最旧的数据
if (leaks.length >= MAX_LEAKS_SIZE) {
leaks.shift(); // 删除最旧的元素
}
leaks.push(new Array(1000000).fill('*')); // 添加新元素
res.end('Memory Leak Test');
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
方案二: 避免全局变量存储: 如果不需要将请求数据存储到全局数组中,可以直接避免分配这些对象。可以直接在请求函数内,定义临时数据,这样在请求处理完毕后会被释放。
const http = require('http');
const server = http.createServer((req, res) => {
// 不存储不必要的数据,避免内存泄漏
const temp = new Array(1000000).fill('*');
res.end('Memory Leak Test');
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
方案三、使用弱引用(WeakMap/WeakRef
): 如果数据的生命周期与请求相关,可以使用弱引用,确保数据不影响垃圾回收。比如: 使用 WeakMap
存储请求数据。WeakMap
的键是弱引用,不会阻止垃圾回收器清理键值对。当 requestKey
失去引用后,数据会被自动清理。
const http = require('http');
const leaks = new WeakMap();
const server = http.createServer((req, res) => {
const requestKey = {};
leaks.set(requestKey, new Array(1000000).fill('*'));
res.end('Memory Leak Test');
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
四、对象引用
五、事件监听器
5.1 EventEmitter 事件监听器
EventEmitter
添加事件监听器后,未适时移除,导致事件堆积,无法释放内存。解决方案如下:
-
设置最大监听器数量,避免无限增长
-
使用
emitter.removeListener
或emitter.off
移除监听器。
const EventEmitter = require('events');
const emitter = new EventEmitter();
function memoryLeak() {
emitter.on('leak', () => {
console.log('Event Triggered');
});
}
setInterval(memoryLeak, 1000); // 每次调用都会注册新的事件监听器
emitter.setMaxListeners(10); // 限制最大监听器数量
六、未关闭资源句柄
未关闭资源句柄: 打开数据库连接、文件或网络请求后,没有正确关闭,导致资源泄漏。
6.1 关闭 fs 句柄
fs.open()
返回的文件句柄一定要通过 fs.close
关闭
const fs = require('fs');
function readFile() {
fs.open('file.txt', 'r', (err, fd) => {
if (err) throw err;
// 忘记关闭文件描述符
});
}
setInterval(readFile, 1000);
const fs = require('fs');
function readFile() {
fs.open('file.txt', 'r', (err, fd) => {
if (err) throw err;
// 通过 fs.close 关闭文件描述符
fs.close(fd, (err) => {
if (err) throw err;
});
});
}
setInterval(readFile, 1000);
6.2 关闭 net 句柄
net.createServer()
返回的网络服务器句柄
6.3 关闭 zlib 句柄
zlib.createGzip()
返回的压缩句柄
6.4 关闭 dgram 句柄
dgram.createSocket()
返回的 UDP socket
句柄
6.5 关闭 mysql 句柄
6.6 关闭 crypto 句柄
crypto.createHash()
返回的哈希句柄
6.7 关闭 socket 句柄
// ❌ 错误做法:监听器没有清理 socket.on('event', handler);
// ✅ 正确做法:保存引用并清理
const handlers = { event: handler };
socket.on('event', handler);
Object.entries(handlers).forEach(([event, handler]) => {
socket.off(event, handler);
});
6.8 关闭 child_process 句柄
child_process.spawn()
返回的子进程句柄
七、错误处理不完善
7.1 正确处理回调错误
异步回调函数出现错误,但未正确释放资源。添加完善的错误处理,确保资源总能被释放。
const fs = require('fs');
function readFile() {
fs.readFile('nonexistent.txt', (err, data) => {
if (err) return; // 错误处理不完善,可能有资源未释放
console.log(data);
});
}
八、强引用 Set/Map
// 不好的做法
const userSessions = new Map();
function manageUserSession(user) {
const session = { loginTime: Date.now(), activities: [] };
userSessions.set(user, session);
// 即使用户登出,Map 仍然保持对 user 对象的引用
}
// 好的做法
const userSessions = new WeakMap();
function manageUserSession(user) {
const session = { loginTime: Date.now(), activities: [] };
userSessions.set(user, session);
// 当用户对象不再被引用时,session 数据会被自动清理
}
九、缓存数据无限存储
应用程序缓存过多的数据,导致内存不断占用。
const cache = {};
function addToCache(key, value) {
cache[key] = value;
}
setInterval(() => {
addToCache(Date.now(), new Array(1000000).fill('*'));
}, 1000);
方案一、使用 LRU(Least Recently Used)
缓存策略:
const LRU = require('lru-cache');
const cache = new LRU({ max: 50 }); // 限制最大缓存数量
cache.set('key', 'value');
十、进程长时间运行未优化
Node.js
长时间运行的服务会积累一些无法释放的内存对象。
10.1 PM2 条件重启
使用 PM2
或类似工具重启进程:
pm2 start app.js --max-memory-restart 200M
这会在内存使用超过 200MB
时重启进程,避免内存泄漏导致崩溃。