1. 前言
Node.js 是一种很流行的服务器端语言,它的内存管理机制和 JavaScript 不一样,这导致了内存泄漏的问题比其他语言更加常见。内存泄漏会导致程序在运行一段时间后会不断增加内存使用量,最终导致程序崩溃。本文将介绍一些排查 Node.js 中内存泄漏的方法。
2. 什么是内存泄漏?
内存泄漏是指一个程序分配了一段内存空间,但在使用完毕后没有将其释放。随着程序运行时间的增加,内存使用量会不断增加,最终导致程序奔溃。
2.1 Node.js 中内存泄漏的种类
Node.js 中的内存泄漏可以分为以下几种:
全局变量泄漏
闭包泄漏
事件监听器泄漏
定时器泄漏
缓存泄漏
3. 发现内存泄漏
在 Node.js 中,发现内存泄漏一般使用以下两种方法:
使用内存快照
使用性能分析工具
3.1 使用内存快照
Node.js 提供了一个内存快照工具 v8-profiler
,这个工具可以生成当前运行时的内存快照。
通过 v8-profiler
我们可以获取内存对象的引用关系,帮助我们找到泄漏对象和引用它的对象。
以下是使用 v8-profiler
的例子:
const profiler = require('v8-profiler');
const fs = require('fs');
profiler.startProfiling();
// 执行一段程序
let profile = profiler.stopProfiling();
let snapshot = profile.export();
fs.writeFileSync('./snapshot.json', JSON.stringify(snapshot));
通过 fs.writeFileSync
将内存快照保存为 JSON 文件,然后通过 Chrome 调试工具打开该文件,就可以看到内存中存在的对象,以及它们之间的引用关系。
3.2 使用性能分析工具
Node.js 自带了一个性能分析工具 profiler
,它可以帮助我们找到程序中 CPU 占用率高的地方,从而找出可能存在内存泄漏的代码。
以下是使用 profiler
的例子:
const profiler = require('profiler');
profiler.startProfiling('CPU Profile');
// 执行一段程序
let profile = profiler.stopProfiling('CPU Profile');
console.log(profile.export());
profile.delete();
通过 console.log(profile.export())
可以将性能分析结果输出到控制台,然后通过 Chrome 调试工具的 Performance 面板来分析。
4. 解决内存泄漏
一旦发现了 Node.js 中的内存泄漏,那么就需要着手解决了。
4.1 全局变量泄漏
全局变量是程序中最容易引起内存泄漏的对象,因为它们一旦被定义,就会一直保持在内存中,直到程序结束。
以下是一个错误示例:
// 错误示例
let globalVar = 'global';
function test() {
// 引用全局变量,导致全局变量无法释放
console.log(globalVar);
}
test();
正确的做法是使用 let
或 const
声明变量,避免把变量定义在全局作用域中:
// 正确示例
function test() {
let localVar = 'local';
console.log(localVar);
}
test();
4.2 闭包泄漏
闭包是指能够访问外部函数作用域的函数。
在 Node.js 中使用闭包时,需要注意不要引起变量泄漏,即让函数引用外部作用域的变量,这会导致变量不能被释放。
以下是一个错误示例:
// 错误示例
function foo() {
let localVar = 'local';
return function () {
console.log(localVar);
};
}
let bar = foo();
bar();
在以上示例中,bar()
函数引用了 foo()
函数作用域中的变量 localVar
,导致该变量无法被释放。
正确的做法是避免函数引用外部函数作用域的变量:
// 正确示例
function foo() {
return function () {
console.log('hello world');
};
}
let bar = foo();
bar();
4.3 事件监听器泄漏
在 Node.js 中,如果使用 EventEmitter
注册了事件监听器,但是在不再需要该监听器时没有移除掉,那么这个监听器就会一直存在于内存中,一直占用着内存。
以下是一个错误示例:
// 错误示例
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', function () {
console.log('an event occurred!');
});
myEmitter.emit('event');
在以上示例中,没有移除掉事件监听器,如果一直使用该对象,就会导致内存泄漏。
正确的做法是移除不再使用的事件监听器:
// 正确示例
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
function listener() {
console.log('an event occurred!');
}
myEmitter.on('event', listener);
myEmitter.emit('event');
// 移除监听器
myEmitter.removeListener('event', listener);
4.4 定时器泄漏
在 Node.js 中,使用定时器时,需要注意在不使用时将定时器清除掉,否则会一直占用内存。在 Node.js 中,使用 clearInterval
、clearTimeout
和 clearImmediate
来清除定时器。
以下是一个错误示例:
// 错误示例
setInterval(function () {
console.log('hello world');
}, 1000);
在以上示例中,没有清除掉定时器,如果一直使用该对象,就会导致内存泄漏。
正确的做法是清除不再使用的定时器:
// 正确示例
let timer = setInterval(function () {
console.log('hello world');
}, 1000);
// 清除定时器
clearInterval(timer);
4.5 缓存泄漏
在 Node.js 中,使用缓存时需要注意不要占用过多的内存,否则会导致程序崩溃。
以下是一个错误示例:
// 错误示例
const cache = {};
function getData(id) {
if (cache[id]) {
// 如果缓存中存在该对象,则直接返回缓存对象
return cache[id];
} else {
// 否则从数据库中获取数据,并存入缓存中
let data = fetchDataFromDB(id);
cache[id] = data;
return data;
}
}
在以上示例中,使用一个全局的 cache
对象来保存缓存数据,在程序运行一定时间后,会占用过多的内存,导致程序崩溃。
正确的做法是使用 Map
或 WeakMap
来缓存数据:
// 正确示例
const cache = new Map();
function getData(id) {
if (cache.has(id)) {
// 如果缓存中存在该对象,则直接返回缓存对象
return cache.get(id);
} else {
// 否则从数据库中获取数据,并存入缓存中
let data = fetchDataFromDB(id);
cache.set(id, data);
return data;
}
}
5. 总结
内存泄漏是程序的薄弱点之一,是每个开发者都必须掌握的技能之一。在 Node.js 中,内存泄漏的种类繁多,但是我们只需要掌握正确的排查方法,和针对性的解决方法就可以解决大部分的内存泄漏问题。