Node.js 中的内存泄漏可能是应用程序的隐形杀手。它们会降低性能、增加成本并最终导致崩溃。让我们分析一下常见原因以及预防或修复它们的可行策略。
1️⃣ 参考文献:隐藏的罪魁祸首
全局变量:意外地将对象分配给全局变量(例如,global.data = ...)会使它们永远保留在内存中。
// Leak Example: Accidentally assigning to global scope
function processUserData(user) {
global.cachedUser = user; // Stored globally, never garbage-collected!
}
修复:使用模块或闭包来封装数据:
// ✅ Safe approach: Module-scoped cache
const userCache = new Map();
function processUserData(user) {
userCache.set(user.id, user);
}
多重引用:其他引用(例如缓存、数组)保留的未使用对象。
// Leak Example: Cached array with lingering references
const cache = [];
function processData(data) {
cache.push(data); // Data remains even if unused!
}
修复:使用 WeakMap 进行短暂引用:
// ✅ WeakMap allows garbage collection when keys are removed
const weakCache = new WeakMap();
function processData(obj) {
weakCache.set(obj, someMetadata); // Auto-cleared if obj is deleted
}
单例:管理不善的单例会积累陈旧的数据。
2️⃣ 闭包和作用域:内存陷阱
递归闭包:循环或递归调用内的函数,捕获外部范围变量。
// Leak Example: Closure in a loop retains outer variables
for (var i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 1000); // All logs print "10"!
}
修复:使用 let 或者 break 闭包:
// ✅ let creates a block-scoped variable
for (let i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 1000); // Logs 0-9
}
require在代码中间:在函数内部动态地要求模块可能会导致重复的模块加载。
// Leak Example: Repeatedly loading a module
function getConfig() {
const config = require('./config.json'); // Re-loaded every call!
return config;
}
修复:在顶部加载一次:
// ✅ Load once, reuse
const config = require('./config.json');
function getConfig() {
return config;
}
3️⃣ 操作系统和语言对象:资源泄漏
打开描述符:未关闭的文件、套接字或数据库连接。
// Leak Example: Forgetting to close a file
fs.open('largefile.txt', 'r', (err, fd) => {
// Read file but never close fd!
});
修复:总是关闭资源:
// ✅ Cleanup with try-finally
fs.open('largefile.txt', 'r', (err, fd) => {
try {
// Read file...
} finally {
fs.close(fd, () => {}); // Ensure cleanup
}
});
setTimeout/setInterval:忘记引用对象的计时器。
// Leak Example: Uncleared interval
const interval = setInterval(() => {
fetchData(); // Runs forever, even if unused!
}, 5000);
修复:完成后清除计时器:
// ✅ Clear interval on cleanup
function startInterval() {
const interval = setInterval(fetchData, 5000);
return () => clearInterval(interval); // Return cleanup function
}
const stopInterval = startInterval();
stopInterval(); // Call when done
4️⃣ 活动与订阅:沉默的积累者
EventEmitter 监听器:不删除监听器。
// Leak Example: Adding listeners without removing
const emitter = new EventEmitter();
emitter.on('data', (data) => process(data)); // Listener persists forever!
修复:始终删除监听器:
// ✅ Use named functions for removal
function onData(data) { process(data); }
emitter.on('data', onData);
emitter.off('data', onData); // Explicit cleanup
过时的回调:将匿名函数传递给事件处理程序(例如,on('data', () => {...}))。
// Leak Example: Anonymous function in event listener
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
setInterval(() => {
myEmitter.on('data', (message) => {
console.log('Received:', message);
});
}, 1000);
setInterval(() => {
myEmitter.emit('data', 'Hello, world!');
}, 3000);
修复:对一次性事件使用 once():
// ✅ Auto-remove after firing
setInterval(() => {
myEmitter.once('data', (message) => {
console.log('Received:', message);
});
}, 1000);
5️⃣ Cache:一把双刃剑
无界缓存:无限增长的缓存。
// Leak Example: Cache with no limits
const cache = new Map();
function getData(key) {
if (!cache.has(key)) {
cache.set(key, fetchData(key)); // Grows forever!
}
return cache.get(key);
}
修复:使用带 TTL 的 LRU 缓存:
// ✅ npm install lru-cache
const LRU = require('lru-cache');
const cache = new LRU({ max: 100, ttl: 60 * 1000 }); // Limit to 100 items, 1min TTL
很少使用的值:缓存从未访问过的条目。
6️⃣ Mixins:有风险的扩展
使用内置函数:向 Object.prototype 或本机类添加方法。
// Leak Example: Adding to Object.prototype
Object.prototype.log = function() { console.log(this); };
// All objects now have `log`, causing confusion and leaks!
修复:改用实用函数:
// ✅ Safe utility module
const logger = {
log: (obj) => console.log(obj)
};
logger.log(user); // No prototype pollution
流程级混合:将数据附加到流程或全局上下文。
7️⃣ 并发:工作线程与进程管理
孤立的工作进程/线程:忘记终止子进程或工作线程。
// Leak Example: Forgetting to terminate a worker
const { Worker } = require('worker_threads');
const worker = new Worker('./task.js');
// Worker runs indefinitely!
解决办法:追踪并解雇工人:
// ✅ Cleanup with a pool
const workers = new Set();
function createWorker() {
const worker = new Worker('./task.js');
workers.add(worker);
worker.on('exit', () => workers.delete(worker));
}
// Terminate all on shutdown
process.on('exit', () => workers.forEach(w => w.terminate()));
集群中的共享状态:多进程设置中的内存重复。
预防专业技巧
堆快照:使用 node --inspect + Chrome DevTools 比较堆快照。
监控事件监听器:使用诸如 emitter.getMaxListeners() 或 EventEmitter.listenerCount() 之类的工具来查找泄漏。
自动清理:使用析构函数、finally 块或 async-exit-hook 等库进行资源清理。
在复杂的系统中,内存泄漏是不可避免的,但只要保持警惕并采取正确的做法,你就可以控制它们。

