跳到主要内容

场景

2024年12月20日
柏拉文
越努力,越幸运

一、定时器


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 全局数组持续新增、插入

全局数组 全局数组持续通过 pushappend 等操作新增、插入数据,而由于存在强引用,这些对象无法被垃圾回收器(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 添加事件监听器后,未适时移除,导致事件堆积,无法释放内存。解决方案如下:

  1. 设置最大监听器数量,避免无限增长

  2. 使用 emitter.removeListeneremitter.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 时重启进程,避免内存泄漏导致崩溃。