5.2 高效使用内存

22 阅读2分钟

这一小节超级实用,朴灵作者从开发者的角度讲解了如何在Node.js中高效使用内存,重点是闭包的内存特性常见的内存泄漏场景。读完这一节,你以后写代码时就会下意识避开很多“隐形内存炸弹”,尤其是线上长期运行的Node服务。

5.2.1 作用域与闭包

作者先复习JS作用域链和闭包的内存影响(这是内存泄漏的根源之一)。

  • 作用域链:函数嵌套时,内部函数能访问外部变量,形成链。
  • 闭包:内部函数引用外部变量,即使外部函数执行完,外部变量也不会被GC回收。

示例(经典闭包内存占用):

function outer() {
  let bigData = new Array(10 * 1024 * 1024).fill('data');  // 80MB大数组
  
  function inner() {
    console.log(bigData.length);  // 引用 bigData
  }
  
  return inner;  // 返回内部函数
}

let leak = outer();  // outer 执行完,但 bigData 因闭包被 inner 引用,无法回收
leak();  // 调用 inner,bigData 一直占用内存

问题:只要 leak 存在,80MB的bigData就永远不释放!

正确做法:用完后置null

leak = null;  // 断开引用,bigData 下次GC时回收

5.2.2 内存泄漏常见场景(书里重点列出的四大坑)

作者总结了Node生产环境中最常见的内存泄漏原因:

  1. 无限增长的缓存

    • 用对象/Map做缓存,不设置上限或过期策略。
    const cache = new Map();
    function addToCache(key, value) {
      cache.set(key, value);  // 永远不删除
    }
    
    • 结果:缓存越来越大,最终OOM。
    • 解决:用LRU缓存(lru-cache库)、设置最大条数、TTL过期。
  2. 全局变量滥用

    • 不小心把大对象挂到global或module.exports。
    • global是根对象,永远不回收。
  3. 未移除的事件监听器

    • EventEmitter 添加监听,但不removeListener。
    server.on('request', hugeHandler);  // hugeHandler 引用大对象
    // 服务器长期运行,监听器越来越多,或大对象无法回收
    
    • 解决:用once(),或显式removeListener,尤其在动态添加时。
  4. 队列消费不匹配(观察者队列积压)

    • 生产者快、消费者慢(如网络慢),事件队列无限增长。
    • 典型:WebSocket推送大数据,客户端消费慢。

作者强调:Node内存泄漏往往缓慢上升(不像Java那么明显),需要长期监控。