一、内存泄漏的那些事儿

咱们程序员最怕什么?代码跑着跑着,内存蹭蹭往上涨,最后直接把服务给干趴了。这种场景在Node.js里特别常见,毕竟它单线程的架构,一旦内存泄漏就是"一锅端"。

举个例子,你写了个简单的HTTP服务:

const http = require('http');

// 错误示范:全局变量存储请求数据
let cache = [];

http.createServer((req, res) => {
  // 每次请求都把body存到全局数组
  let body = '';
  req.on('data', chunk => body += chunk);
  req.on('end', () => {
    cache.push(body);  // 内存泄漏点!
    res.end('OK');
  });
}).listen(3000);

看见没?这个cache数组会无限制增长,请求越多内存占用越大。这就是典型的内存泄漏——该释放的内存没释放。

二、如何揪出内存泄漏

2.1 Chrome DevTools大法

Node.js和Chrome同宗同源,调试工具都是通用的:

# 启动时加上--inspect
node --inspect server.js

然后在Chrome地址栏输入chrome://inspect,抓取内存快照。重点看:

  • Retained Size大的对象
  • 重复出现的相似对象
  • 脱离DOM树的游离节点(前端场景)

2.2 内存dump分析

heapdump模块拍快照:

const heapdump = require('heapdump');

// 手动触发dump
setInterval(() => {
  heapdump.writeSnapshot((err, filename) => {
    console.log(`Dump saved to ${filename}`);
  });
}, 60 * 1000);  // 每分钟dump一次

clinic.js工具分析:

clinic heapdoctor -- node server.js

三、常见泄漏场景与修复

3.1 闭包陷阱

看这段代码:

function createClosure() {
  const hugeArray = new Array(1000000).fill('*');
  return function() {
    console.log('This holds the array!');
  };
}

setInterval(() => {
  const fn = createClosure();
  // fn虽然没执行,但hugeArray被闭包引用着
}, 1000);

修复方案:

function safeClosure() {
  const tempArray = new Array(1000000).fill('*');
  // 使用完后主动释放
  return function() {
    tempArray.length = 0;  // 清空数组
    console.log('Array cleared');
  };
}

3.2 事件监听泄漏

const EventEmitter = require('events');
const emitter = new EventEmitter();

function createListener() {
  emitter.on('ping', () => {
    console.log('Memory leaking...');
  });
}

// 每次调用都新增监听器
setInterval(createListener, 1000);

正确做法:

// 方案1:使用once替代on
emitter.once('ping', handler);

// 方案2:主动移除监听器
const handler = () => {...};
emitter.on('ping', handler);
emitter.removeListener('ping', handler);

四、高级防御策略

4.1 内存限制

启动时设置内存上限:

node --max-old-space-size=512 server.js

程序内监控:

setInterval(() => {
  const usage = process.memoryUsage();
  if (usage.heapUsed > 500 * 1024 * 1024) {
    console.error('Memory overload!');
    process.exit(1);  // 自杀保平安
  }
}, 5000);

4.2 Worker线程隔离

把危险操作放到Worker线程:

const { Worker } = require('worker_threads');

function runTask() {
  const worker = new Worker(`
    const { parentPort } = require('worker_threads');
    // 内存密集型操作放这里
    parentPort.postMessage('done');
  `);
  
  worker.on('exit', () => {
    console.log('Worker terminated');
  });
}

五、实战经验总结

  1. 定时重启:用pm2设置定时重启
  2. 监控报警:接入Grafana监控堆内存
  3. 代码规范
    • 避免大对象全局存储
    • 及时清理定时器
    • 慎用第三方库(有些库会悄悄缓存数据)

记住,内存泄漏就像房间里的垃圾,不打扫就会越堆越多。定期"大扫除",才能让Node.js应用跑得又快又稳!