您现在的位置是:网站首页 > 内存泄漏排查文章详情

内存泄漏排查

内存泄漏的基本概念

内存泄漏指的是程序中已分配的内存未能被正确释放,导致可用内存逐渐减少。在Node.js中,由于V8引擎的内存管理机制和事件循环的特性,内存泄漏问题可能表现得更为隐蔽。常见的内存泄漏场景包括未清理的定时器、闭包引用、全局变量滥用以及事件监听器未移除等。

常见的内存泄漏场景

未清理的定时器

function leakyFunction() {
  setInterval(() => {
    console.log('This interval keeps running...');
  }, 1000);
}

leakyFunction();

这个例子中,setInterval创建的定时器会持续运行,即使leakyFunction已经执行完毕。如果没有显式调用clearInterval,这个定时器会一直存在,导致内存泄漏。

闭包引用

function createClosure() {
  const hugeArray = new Array(1000000).fill('*');
  return function() {
    console.log(hugeArray.length);
  };
}

const closure = createClosure();

这里返回的闭包函数持有了对hugeArray的引用,即使外部不再需要这个数组,它也无法被垃圾回收。

未移除的事件监听器

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

function addListener() {
  emitter.on('event', () => {
    console.log('Event handled');
  });
}

addListener();
// 之后没有调用 emitter.off('event')

每次调用addListener都会添加一个新的事件监听器,如果不手动移除,这些监听器会一直存在于内存中。

内存泄漏的检测工具

Chrome DevTools

  1. 启动Node.js时添加--inspect标志:
    node --inspect your-script.js
    
  2. 打开Chrome浏览器,访问chrome://inspect
  3. 点击"Open dedicated DevTools for Node"
  4. 在"Memory"标签页中可以进行堆内存快照

heapdump模块

const heapdump = require('heapdump');

// 手动触发堆内存转储
heapdump.writeSnapshot('/tmp/' + Date.now() + '.heapsnapshot');

Node.js内置的process.memoryUsage()

setInterval(() => {
  const memoryUsage = process.memoryUsage();
  console.log({
    rss: memoryUsage.rss / 1024 / 1024 + 'MB',
    heapTotal: memoryUsage.heapTotal / 1024 / 1024 + 'MB',
    heapUsed: memoryUsage.heapUsed / 1024 / 1024 + 'MB',
    external: memoryUsage.external / 1024 / 1024 + 'MB',
  });
}, 1000);

分析内存快照

  1. 在Chrome DevTools中比较两个时间点的堆快照
  2. 关注"Retainers"面板,查看对象的引用链
  3. 查找意外保留的大对象
  4. 特别注意:
    • Detached DOM树(在浏览器环境中)
    • 闭包
    • 缓存对象
    • 全局变量

内存泄漏的修复策略

定时器清理

let intervalId;

function startInterval() {
  intervalId = setInterval(() => {
    console.log('Running...');
  }, 1000);
}

function stopInterval() {
  clearInterval(intervalId);
}

正确管理事件监听器

class MyComponent {
  constructor() {
    this.handleEvent = this.handleEvent.bind(this);
    emitter.on('event', this.handleEvent);
  }

  handleEvent() {
    console.log('Event received');
  }

  destroy() {
    emitter.off('event', this.handleEvent);
  }
}

避免全局变量

// 不好的做法
function storeData() {
  global.cache = {};
}

// 更好的做法
class DataStore {
  constructor() {
    this.cache = {};
  }

  clear() {
    this.cache = null;
  }
}

流和缓冲区管理

const fs = require('fs');

function readFile() {
  const stream = fs.createReadStream('large-file.txt');
  let data = '';
  
  stream.on('data', (chunk) => {
    data += chunk; // 可能导致内存问题
  });

  // 更好的方式
  const chunks = [];
  stream.on('data', (chunk) => {
    chunks.push(chunk);
  });
  
  stream.on('end', () => {
    const buffer = Buffer.concat(chunks);
    // 处理buffer
  });
}

内存泄漏预防的最佳实践

  1. 使用WeakMapWeakSet存储临时引用

    const weakMap = new WeakMap();
    let obj = {};
    weakMap.set(obj, 'some data');
    obj = null; // 当obj被垃圾回收时,WeakMap中的条目会自动移除
    
  2. 限制缓存大小并实现淘汰策略

    class LRUCache {
      constructor(maxSize) {
        this.maxSize = maxSize;
        this.cache = new Map();
      }
      
      get(key) {
        const value = this.cache.get(key);
        if (value) {
          this.cache.delete(key);
          this.cache.set(key, value);
        }
        return value;
      }
      
      set(key, value) {
        if (this.cache.size >= this.maxSize) {
          const firstKey = this.cache.keys().next().value;
          this.cache.delete(firstKey);
        }
        this.cache.set(key, value);
      }
    }
    
  3. 使用内存监控中间件

    function memoryMonitor(req, res, next) {
      const startMemory = process.memoryUsage();
      
      res.on('finish', () => {
        const endMemory = process.memoryUsage();
        const delta = endMemory.heapUsed - startMemory.heapUsed;
        console.log(`Memory delta: ${delta} bytes`);
      });
      
      next();
    }
    
    app.use(memoryMonitor);
    
  4. 定期压力测试和内存分析

    const autocannon = require('autocannon');
    const { promisify } = require('util');
    const run = promisify(autocannon);
    
    async function stressTest() {
      await run({
        url: 'http://localhost:3000',
        connections: 100,
        duration: 60
      });
      
      // 测试完成后立即进行堆快照
      const heapdump = require('heapdump');
      heapdump.writeSnapshot();
    }
    

Node.js特定场景的内存管理

集群模式下的内存泄漏

const cluster = require('cluster');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork();
  });
} else {
  // Worker代码
  require('./server');
  
  // 监控内存使用
  setInterval(() => {
    if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
      process.exit(1); // 内存超过500MB时重启
    }
  }, 5000);
}

大JSON处理

// 不好的做法
const data = JSON.parse(fs.readFileSync('large.json', 'utf8'));

// 更好的做法
const stream = fs.createReadStream('large.json');
const jsonStream = require('JSONStream');
const parser = jsonStream.parse('*');

stream.pipe(parser)
  .on('data', (item) => {
    // 逐项处理
  })
  .on('end', () => {
    console.log('Finished processing');
  });

数据库连接泄漏

const { Pool } = require('pg');
const pool = new Pool({ max: 20 });

async function queryDatabase() {
  let client;
  try {
    client = await pool.connect();
    const result = await client.query('SELECT * FROM users');
    return result.rows;
  } finally {
    if (client) client.release(); // 确保连接被释放
  }
}

长期运行进程的特殊考虑

对于需要长期运行的Node.js进程(如服务器应用),还需要考虑:

  1. 内存碎片化问题
  2. V8引擎的内存限制(默认约1.4GB)
  3. 外部内存(如Buffer)不计入V8堆内存
  4. 使用--max-old-space-size调整内存限制
    node --max-old-space-size=4096 server.js
    

真实案例分析

一个Express应用的内存泄漏问题:

const express = require('express');
const app = express();
const requests = []; // 问题所在

app.get('/leaky', (req, res) => {
  requests.push({
    url: req.url,
    date: new Date()
  });
  
  res.send('OK');
});

app.listen(3000);

问题在于requests数组会无限增长。修复方案:

const express = require('express');
const app = express();
const requests = new Map();
const MAX_REQUESTS = 1000;

app.get('/fixed', (req, res) => {
  if (requests.size >= MAX_REQUESTS) {
    const oldestKey = requests.keys().next().value;
    requests.delete(oldestKey);
  }
  
  requests.set(Date.now(), {
    url: req.url,
    date: new Date()
  });
  
  res.send('OK');
});

上一篇: 性能分析工具

下一篇: CPU性能分析

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步