「经验总结 」Node怎么排查内存泄漏?思路分享

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();

正确的做法是使用 letconst 声明变量,避免把变量定义在全局作用域中:

// 正确示例

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 中,使用 clearIntervalclearTimeoutclearImmediate 来清除定时器。

以下是一个错误示例:

// 错误示例

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 对象来保存缓存数据,在程序运行一定时间后,会占用过多的内存,导致程序崩溃。

正确的做法是使用 MapWeakMap 来缓存数据:

// 正确示例

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 中,内存泄漏的种类繁多,但是我们只需要掌握正确的排查方法,和针对性的解决方法就可以解决大部分的内存泄漏问题。