{T}

内存管理

概述

Node.js 基于 V8 引擎,继承了其内存管理机制。理解 Node.js 的内存模型对于构建高性能、稳定的应用至关重要。本文档详细介绍 Node.js 内存机制、垃圾回收原理、内存泄漏检测方法及优化策略。

Node.js 内存机制

V8 内存架构

V8 引擎的堆内存分为几个主要区域,每个区域负责存储不同类型的对象:

code
┌─────────────────────────────────────────────────────────────┐
│                      V8 堆内存结构                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌────────────────────────────────────────────────────┐    │
│  │  新生代 (New Space / Young Generation)              │    │
│  │  ┌──────────────┬──────────────┐                  │    │
│  │  │  From Space  │   To Space   │                  │    │
│  │  │   (活动对象)  │   (空闲空间)  │                  │    │
│  │  └──────────────┴──────────────┘                  │    │
│  │  • 存放短生存期对象                                  │    │
│  │  • Scavenge 算法回收                               │    │
│  │  • 大小: 1-8MB (可通过 --max-new-space-size 调整)   │    │
│  │  • 对象晋升: 经历多次 GC 后晋升到老生代              │    │
│  └────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌────────────────────────────────────────────────────┐    │
│  │  老生代 (Old Space / Old Generation)               │    │
│  │  • 存放长生存期对象                                  │    │
│  │  • Mark-Sweep-Compact 算法回收                     │    │
│  │  • 大小: 动态 (可通过 --max-old-space-size 调整)    │    │
│  │  • 分为: 指针空间、数据空间                         │    │
│  └────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌────────────────────────────────────────────────────┐    │
│  │  大对象空间 (Large Object Space)                    │    │
│  │  • 存放超过 256KB 的大对象                           │    │
│  │  • 直接分配,不经过新生代                            │    │
│  │  • 独立管理,避免复制开销                            │    │
│  └────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌────────────────────────────────────────────────────┐    │
│  │  代码空间 (Code Space)                              │    │
│  │  • 存储 JIT 编译后的代码                             │    │
│  │  • 唯一拥有执行权限的内存空间                        │    │
│  └────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌────────────────────────────────────────────────────┐    │
│  │  Map 空间 (Map Space)                               │    │
│  │  • 存储对象类型信息(隐藏类)                        │    │
│  │  • 用于快速属性访问                                  │    │
│  └────────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

内存限制及原因

Node.js 默认内存限制:

系统架构默认内存限制说明
64 位系统约 1.4 GBV8 引擎限制
32 位系统约 0.7 GBV8 引擎限制

限制原因

  1. 垃圾回收效率:内存越大,GC 耗时越长,可能导致应用暂停
  2. 浏览器场景:V8 最初为浏览器设计,单个网页不需要太多内存
  3. 一致性:保持不同平台行为一致

process.memoryUsage API 详解

javascript
// 获取内存使用情况
const used = process.memoryUsage();

console.log({
  // 常驻集大小 (Resident Set Size)
  // 包含所有内存:堆、栈、代码段等
  rss: `${Math.round(used.rss / 1024 / 1024)} MB`,
  
  // V8 分配的堆内存总量
  heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`,
  
  // V8 堆内存使用量
  heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`,
  
  // 外部内存使用量(如 Buffer)
  // 不在 V8 堆中分配
  external: `${Math.round(used.external / 1024 / 1024)} MB`,
  
  // Node.js 12.17+ 新增
  // 数组缓冲区使用的内存
  arrayBuffers: `${Math.round(used.arrayBuffers / 1024 / 1024)} MB`
});

启动参数配置

bash
# 常用内存配置参数

# 设置老生代内存上限(单位:MB)
node --max-old-space-size=4096 app.js

# 设置新生代内存大小(单位:MB)
node --max-new-space-size=256 app.js

# 设置半空间大小(Scavenge 算法使用)
node --max-semi-space-size=128 app.js

# 启用内存压缩(节省内存,但增加 CPU 使用)
node --memory-reducer

# 禁用增量标记(可能影响 GC 性能)
node --no-incremental-marking

# 优化建议
# - 老生代: 根据实际需求设置,一般不超过系统内存的 75%
# - 新生代: 默认值通常足够,频繁创建短生命周期对象时可适当增大

垃圾回收机制

GC 算法详解

1. Scavenge 算法(新生代)

code
初始状态:
┌──────────────┬──────────────┐
│  From Space  │   To Space   │
│  [A][B][C]   │    [ ]       │
└──────────────┴──────────────┘

GC 过程:
1. 从 From 空间扫描存活对象
2. 复制存活对象到 To 空间
3. 清空 From 空间

复制后:
┌──────────────┬──────────────┐
│  From Space  │   To Space   │
│    [ ]       │  [A][B][C]   │
└──────────────┴──────────────┘

4. 交换 From 和 To 角色

特点

  • 时间复杂度:O(存活对象数量)
  • 空间利用率:约 50%(双倍空间)
  • 适合新生代:对象死亡率高,存活数量少

2. Mark-Sweep-Compact 算法(老生代)

code
Mark 标记阶段:
┌────────────────────────────────┐
│ [A][B][ ][C][ ][D][E][ ][F]    │  标记存活对象
│  ✓     ✓  ✓  ✓        ✓       │
└────────────────────────────────┘

Sweep 清除阶段:
┌────────────────────────────────┐
│ [A][ ][ ][C][ ][D][E][ ][F]    │  清除未标记对象
│  ✓     ✓  ✓  ✓        ✓       │  B 被清除
└────────────────────────────────┘

Compact 整理阶段(可选):
┌────────────────────────────────┐
│ [A][C][D][E][F][ ][ ][ ][ ]    │  移动存活对象
│  ✓  ✓  ✓  ✓  ✓                │  消除内存碎片
└────────────────────────────────┘

特点

  • Mark-Sweep:快速,但产生内存碎片
  • Compact:消除碎片,但耗时较长
  • 增量标记:将标记分解为多个小步,减少暂停时间

查看 GC 日志

bash
# 启用 GC 日志
node --trace-gc app.js

# 输出示例
# [13965:0x103800000]    12345 ms: Scavenge 2.4 (3.1) -> 1.8 (4.1) MB
#                        ↑ GC 类型  ↑ 前后内存变化

# GC 类型说明:
# - Scavenge: 新生代快速回收
# - Mark-sweep: 老生代标记清除
# - Mark-sweep compact: 老生代标记清除整理
# - Incremental marking: 增量标记

手动触发 GC

javascript
// 强制触发垃圾回收(仅用于测试和调试)
// 启动参数: node --expose-gc app.js

if (global.gc) {
  // 手动触发一次完整 GC
  global.gc();
  console.log('手动触发 GC 完成');
}

// 注意:生产环境不建议手动触发 GC
// V8 的自动 GC 通常更高效

对象晋升

对象从新生代晋升到老生代的条件:

javascript
/**
 * 晋升条件:
 * 1. 对象经历过多次 Scavenge GC(默认 2 次)
 * 2. To 空间使用率超过 25%
 * 
 * 晋升流程:
 * 新生代 (Scavenge) -> 判断晋升条件 -> 老生代 (Mark-Sweep)
 */

// 示例:观察对象晋升
function testPromotion() {
  const longLivedObject = { data: new Array(1000).fill('x') };
  
  // 短期对象会被快速回收
  for (let i = 0; i < 10000; i++) {
    const temp = { value: i };
  }
  
  // longLivedObject 经历多次 GC 后会晋升到老生代
  return longLivedObject;
}

内存泄漏检测

常见内存泄漏场景

1. 全局变量泄漏

javascript
// ❌ 错误示例:全局变量持续增长
global.cache = {};
function leak(data) {
  global.cache[data.id] = data; // 无限制增长
}

// ✅ 正确做法:使用有限制的缓存
const cache = new Map();
const MAX_CACHE_SIZE = 1000;

function setCache(key, value) {
  // 达到上限时删除最早的条目
  if (cache.size >= MAX_CACHE_SIZE) {
    const firstKey = cache.keys().next().value;
    cache.delete(firstKey);
  }
  cache.set(key, value);
}

// ✅ 更好方案:使用 LRU 缓存
const LRU = require('lru-cache');
const lruCache = new LRU({
  max: 1000,           // 最大条目数
  maxAge: 1000 * 60 * 5 // 5 分钟过期
});

2. 闭包泄漏

javascript
// ❌ 错误示例:闭包持有大对象引用
function createClosure() {
  const largeData = new Array(1000000).fill('x');
  
  return function() {
    // 大对象无法释放
    console.log(largeData.length);
  };
}

// ✅ 正确做法:只保留必要信息
function createClosure() {
  const length = 1000000; // 只保留需要的值
  
  return function() {
    console.log(length);
  };
}

// ✅ 使用 WeakMap 存储私有数据
const privateData = new WeakMap();

class MyClass {
  constructor() {
    privateData.set(this, {
      largeData: new Array(1000000).fill('x')
    });
  }
  
  // 对象被回收时,WeakMap 中的数据也会自动释放
}

3. 事件监听器泄漏

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

// ❌ 错误示例:重复添加监听器
function setupHandler() {
  emitter.on('data', (data) => {
    process(data);
  });
  // 每次调用都添加新监听器,从不移除
}

// ✅ 正确做法 1:使用 once
emitter.once('data', (data) => {
  process(data); // 自动移除
});

// ✅ 正确做法 2:保存引用并移除
const handler = (data) => {
  process(data);
  emitter.removeListener('data', handler);
};
emitter.on('data', handler);

// ✅ 正确做法 3:使用 AbortController (Node.js 15.4+)
const ac = new AbortController();
emitter.on('data', handler, { signal: ac.signal });

// 需要时取消
ac.abort();

4. 定时器泄漏

javascript
// ❌ 错误示例:未清除定时器
function startTimer() {
  setInterval(() => {
    fetchData();
  }, 1000);
  // 定时器永不停止,即使对象不再需要
}

// ✅ 正确做法:保存引用并清除
class DataManager {
  constructor() {
    this.timer = null;
  }
  
  start() {
    this.timer = setInterval(() => {
      this.fetchData();
    }, 1000);
  }
  
  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }
}

// ✅ 使用清理函数
const timers = new Set();

function addTimer(callback, delay) {
  const id = setInterval(callback, delay);
  timers.add(id);
  return id;
}

function clearAllTimers() {
  timers.forEach(id => clearInterval(id));
  timers.clear();
}

5. Promise 未处理泄漏

javascript
// ❌ 错误示例:Promise 链未处理
async function leak() {
  const promise = fetchData()
    .then(data => process(data));
  // 没有 await 或 catch,错误可能被忽略
}

// ✅ 正确做法:正确处理 Promise
async function correct() {
  try {
    const data = await fetchData();
    return process(data);
  } catch (error) {
    console.error('处理失败:', error);
    throw error;
  }
}

// ✅ 全局未处理 Promise 拒绝监听
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise 拒绝:', reason);
  // 可选:发送到错误监控系统
});

6. Map 和 Set 泄漏

javascript
// ❌ 错误示例:Map 持续增长
const cache = new Map();
function cacheData(key, value) {
  cache.set(key, value);
  // 从不删除,持续增长
}

// ✅ 正确做法 1:使用 WeakMap
const weakCache = new WeakMap();
function cacheObject(obj, value) {
  weakCache.set(obj, value);
  // obj 被 GC 时,缓存自动清理
}

// ✅ 正确做法 2:定期清理
setInterval(() => {
  for (const [key, value] of cache.entries()) {
    if (isExpired(value)) {
      cache.delete(key);
    }
  }
}, 60000);

使用 heapdump 检测

bash
npm install heapdump
javascript
const heapdump = require('heapdump');
const path = require('path');

// 手动生成堆快照
heapdump.writeSnapshot(
  path.join(__dirname, `heapdump-${Date.now()}.heapsnapshot`),
  (err, filename) => {
    if (err) console.error('生成快照失败:', err);
    else console.log('堆快照已保存:', filename);
  }
);

// 内存超阈值自动生成
let lastDumpTime = 0;
const DUMP_INTERVAL = 60000; // 最小间隔 1 分钟

setInterval(() => {
  const used = process.memoryUsage();
  const heapUsedMB = used.heapUsed / 1024 / 1024;
  
  if (heapUsedMB > 500 && Date.now() - lastDumpTime > DUMP_INTERVAL) {
    const filename = `heapdump-${Date.now()}.heapsnapshot`;
    heapdump.writeSnapshot(
      path.join(__dirname, filename),
      (err, file) => {
        if (!err) {
          console.log('内存超限,已生成快照:', file);
          lastDumpTime = Date.now();
        }
      }
    );
  }
}, 60000);

// 信号触发快照(生产环境推荐)
process.on('SIGUSR2', () => {
  const filename = `heapdump-${Date.now()}.heapsnapshot`;
  heapdump.writeSnapshot(path.join(__dirname, filename));
});

使用 Chrome DevTools 分析

bash
# 启用调试模式
node --inspect app.js

# 在第一行断点(调试启动问题)
node --inspect-brk app.js

# 同时启用 GC 追踪
node --inspect --trace-gc app.js

分析步骤

  1. 打开 Chrome 浏览器,访问 chrome://inspect
  2. 点击 "Open dedicated DevTools for Node"
  3. 进入 Memory 标签
  4. 选择快照类型:
    • Heap snapshot:堆内存快照
    • Allocation instrumentation on timeline:记录内存分配时间线
    • Allocation sampling:采样记录内存分配
  5. 分析策略:
    • 对比多个快照,找出持续增长的对象
    • 使用 "Comparison" 视图对比两个快照
    • 按 Retained Size 排序找出占用内存最大的对象
    • 查看 GC roots 了解对象为何无法释放

使用 clinic.js 工具

bash
# 安装 clinic.js
npm install -g clinic

# 内存泄漏检测
clinic heapprofiler -- node app.js

# 生成报告
# 报告会在浏览器中自动打开

# 检测 I/O 问题
clinic doctor -- node app.js

# 综合诊断
clinic bubbleprof -- node app.js

使用 memwatch-next

bash
npm install memwatch-next
javascript
const memwatch = require('memwatch-next');

// 监控 GC 统计信息
memwatch.on('stats', (stats) => {
  console.log('GC 统计:', {
    num_full_gc: stats.num_full_gc,        // 完整 GC 次数
    num_inc_gc: stats.num_inc_gc,          // 增量 GC 次数
    heap_compactions: stats.heap_compactions, // 堆整理次数
    estimated_base: stats.estimated_base,   // 预估基线内存
    current_base: stats.current_base,       // 当前基线内存
    min: stats.min,                         // 最小内存
    max: stats.max                          // 最大内存
  });
});

// 检测内存泄漏(连续 5 次 GC 内存增长)
memwatch.on('leak', (info) => {
  console.error('检测到内存泄漏:', info);
  // { growth: 12345, reason: 'heap growth over 5 consecutive GCs' }
  
  // 自动生成快照
  const heapdump = require('heapdump');
  heapdump.writeSnapshot(`leak-${Date.now()}.heapsnapshot`);
});

// 堆差异比较
const hd = new memwatch.HeapDiff();
// ... 执行可能泄漏的操作
const diff = hd.end();

console.log('堆内存变化:', {
  before: diff.before,
  after: diff.after,
  change: diff.change
});

内存优化技巧

1. 使用 Buffer 处理二进制数据

javascript
// ❌ 低效:字符串拼接
let str = '';
for (let i = 0; i < 100000; i++) {
  str += 'data' + i; // 每次创建新字符串
}

// ✅ 高效:使用 Buffer 数组
const buffers = [];
for (let i = 0; i < 100000; i++) {
  buffers.push(Buffer.from('data' + i));
}
const result = Buffer.concat(buffers);

// ✅ 更高效:预分配 Buffer
const size = 100000 * 8; // 预估大小
const buffer = Buffer.alloc(size);
let offset = 0;
for (let i = 0; i < 100000; i++) {
  const chunk = Buffer.from('data' + i);
  chunk.copy(buffer, offset);
  offset += chunk.length;
}

2. 使用 Stream 处理大数据

javascript
const fs = require('fs');
const zlib = require('zlib');
const { pipeline } = require('stream');

// ❌ 低效:一次性加载全部内容
fs.readFile('large.txt', (err, data) => {
  if (err) throw err;
  zlib.gzip(data, (err, compressed) => {
    if (err) throw err;
    fs.writeFile('large.txt.gz', compressed, (err) => {
      if (err) throw err;
      console.log('完成');
    });
  });
});

// ✅ 高效:使用流式处理
pipeline(
  fs.createReadStream('large.txt'),
  zlib.createGzip(),
  fs.createWriteStream('large.txt.gz'),
  (err) => {
    if (err) {
      console.error('处理失败:', err);
    } else {
      console.log('完成');
    }
  }
);

// ✅ 流处理示例:逐行处理大文件
const readline = require('readline');

async function processLargeFile(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });
  
  for await (const line of rl) {
    // 逐行处理,内存占用极低
    await processLine(line);
  }
}

3. 对象池模式

javascript
/**
 * 对象池:复用对象,减少 GC 压力
 * 适用于频繁创建销毁相同类型对象的场景
 */
class ObjectPool {
  /**
   * @param {Function} factory - 创建新对象的工厂函数
   * @param {Function} reset - 重置对象状态的函数
   * @param {number} maxSize - 池最大容量
   */
  constructor(factory, reset, maxSize = 100) {
    this.factory = factory;
    this.reset = reset;
    this.maxSize = maxSize;
    this.pool = [];
  }
  
  // 获取对象
  acquire() {
    return this.pool.length > 0 ? this.pool.pop() : this.factory();
  }
  
  // 释放对象回池
  release(obj) {
    if (this.pool.length < this.maxSize) {
      this.reset(obj);
      this.pool.push(obj);
    }
  }
  
  // 获取池状态
  get stats() {
    return {
      available: this.pool.length,
      maxSize: this.maxSize
    };
  }
}

// 使用示例:Buffer 池
const bufferPool = new ObjectPool(
  () => Buffer.alloc(1024),
  (buf) => buf.fill(0),
  100
);

// 使用
const buf = bufferPool.acquire();
try {
  // 使用 buffer 处理数据
  buf.write('hello world');
  console.log(buf.toString());
} finally {
  bufferPool.release(buf);
}

// 使用示例:对象池
const requestPool = new ObjectPool(
  () => ({ method: '', url: '', headers: {} }),
  (obj) => {
    obj.method = '';
    obj.url = '';
    obj.headers = {};
  },
  50
);

4. 使用 WeakMap 和 WeakSet

javascript
// WeakMap 的键必须是对象,且不会阻止垃圾回收
const privateData = new WeakMap();

class User {
  constructor(name) {
    this.name = name;
    // 存储私有数据
    privateData.set(this, {
      token: generateToken(),
      lastAccess: Date.now()
    });
  }
  
  getToken() {
    return privateData.get(this).token;
  }
}

// User 对象被 GC 时,WeakMap 中的数据自动释放

// WeakSet 示例:跟踪已处理对象
const processed = new WeakSet();

function processOnce(obj) {
  if (processed.has(obj)) {
    return; // 已处理过
  }
  
  // 处理对象
  doSomething(obj);
  processed.add(obj);
}

5. 优化数据结构

javascript
// ❌ 低效:使用对象存储大量数据
const data = {};
for (let i = 0; i < 100000; i++) {
  data[`key${i}`] = { value: i };
}

// ✅ 高效:使用 Map
const dataMap = new Map();
for (let i = 0; i < 100000; i++) {
  dataMap.set(`key${i}`, { value: i });
}

// ✅ 数值键使用数组
const dataArr = new Array(100000);
for (let i = 0; i < 100000; i++) {
  dataArr[i] = { value: i };
}

// ✅ 使用 TypedArray 处理数值数据
const numbers = new Float64Array(100000);
for (let i = 0; i < 100000; i++) {
  numbers[i] = i * 1.5;
}

监控与告警

内存监控中间件

javascript
/**
 * Express 内存监控中间件
 */
function memoryMonitor(options = {}) {
  const {
    warnThreshold = 300,    // 警告阈值 (MB)
    errorThreshold = 500,   // 错误阈值 (MB)
    logInterval = 60000     // 日志间隔 (ms)
  } = options;
  
  let lastLogTime = 0;
  
  return function(req, res, next) {
    const used = process.memoryUsage();
    const heapUsedMB = used.heapUsed / 1024 / 1024;
    
    // 设置响应头
    res.set('X-Memory-Usage', `${heapUsedMB.toFixed(2)}MB`);
    res.set('X-Heap-Total', `${(used.heapTotal / 1024 / 1024).toFixed(2)}MB`);
    
    // 内存过高时记录
    if (heapUsedMB > warnThreshold) {
      const now = Date.now();
      if (now - lastLogTime > logInterval) {
        console.warn(`[内存警告] 堆内存使用: ${heapUsedMB.toFixed(2)}MB`);
        lastLogTime = now;
      }
      
      // 内存严重过高
      if (heapUsedMB > errorThreshold) {
        console.error(`[内存严重] 堆内存超过 ${errorThreshold}MB`);
        // 可选:发送告警
        sendAlert(`内存使用严重过高: ${heapUsedMB.toFixed(2)}MB`);
      }
    }
    
    next();
  };
}

// 使用
const express = require('express');
const app = express();

app.use(memoryMonitor({
  warnThreshold: 300,
  errorThreshold: 500
}));

定期内存检查

javascript
class MemoryMonitor {
  constructor(options = {}) {
    this.checkInterval = options.checkInterval || 60000; // 检查间隔
    this.warnThreshold = options.warnThreshold || 0.7;   // 警告阈值 (70%)
    this.alertThreshold = options.alertThreshold || 0.9; // 告警阈值 (90%)
    this.history = [];                                   // 历史记录
    this.maxHistory = options.maxHistory || 60;          // 最大历史记录数
    this.timer = null;
  }
  
  start() {
    this.timer = setInterval(() => {
      this.check();
    }, this.checkInterval);
  }
  
  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }
  
  check() {
    const used = process.memoryUsage();
    const heapUsedMB = used.heapUsed / 1024 / 1024;
    const heapTotalMB = used.heapTotal / 1024 / 1024;
    const rssMB = used.rss / 1024 / 1024;
    const usage = heapUsedMB / heapTotalMB;
    
    const record = {
      timestamp: new Date(),
      heapUsed: heapUsedMB,
      heapTotal: heapTotalMB,
      rss: rssMB,
      usage: usage
    };
    
    // 记录历史
    this.history.push(record);
    if (this.history.length > this.maxHistory) {
      this.history.shift();
    }
    
    // 输出日志
    console.log(
      `[内存监控] 堆: ${heapUsedMB.toFixed(2)}MB / ${heapTotalMB.toFixed(2)}MB ` +
      `(${(usage * 100).toFixed(2)}%) RSS: ${rssMB.toFixed(2)}MB`
    );
    
    // 警告
    if (usage > this.warnThreshold) {
      console.warn(`[内存警告] 内存使用率超过 ${(this.warnThreshold * 100)}%`);
    }
    
    // 告警
    if (usage > this.alertThreshold) {
      this.sendAlert(record);
    }
    
    return record;
  }
  
  sendAlert(record) {
    // 实现告警逻辑(邮件、短信、webhook 等)
    console.error(`[内存告警] 内存使用率超过 ${(this.alertThreshold * 100)}%`);
    // sendEmail(`内存告警: ${record.heapUsed.toFixed(2)}MB`);
    // sendWebhook(record);
  }
  
  getStats() {
    if (this.history.length === 0) return null;
    
    const avgUsage = this.history.reduce((sum, r) => sum + r.usage, 0) / this.history.length;
    const maxUsage = Math.max(...this.history.map(r => r.usage));
    const minUsage = Math.min(...this.history.map(r => r.usage));
    
    return {
      current: this.history[this.history.length - 1],
      average: avgUsage,
      max: maxUsage,
      min: minUsage,
      trend: this.calculateTrend()
    };
  }
  
  calculateTrend() {
    if (this.history.length < 10) return 'insufficient data';
    
    const recent = this.history.slice(-10);
    const avg = recent.reduce((sum, r) => sum + r.heapUsed, 0) / recent.length;
    const first = recent[0].heapUsed;
    const last = recent[recent.length - 1].heapUsed;
    
    if (last > avg * 1.1) return 'increasing';
    if (last < avg * 0.9) return 'decreasing';
    return 'stable';
  }
}

// 使用示例
const monitor = new MemoryMonitor({
  checkInterval: 60000,
  warnThreshold: 0.7,
  alertThreshold: 0.9
});

monitor.start();

// 优雅关闭
process.on('SIGTERM', () => {
  monitor.stop();
  process.exit(0);
});

Prometheus 指标导出

javascript
const client = require('prom-client');

// 创建指标
const heapUsedGauge = new client.Gauge({
  name: 'nodejs_heap_used_bytes',
  help: '堆内存使用量(字节)',
});

const heapTotalGauge = new client.Gauge({
  name: 'nodejs_heap_total_bytes',
  help: '堆内存总量(字节)',
});

const rssGauge = new client.Gauge({
  name: 'nodejs_rss_bytes',
  help: '常驻内存大小(字节)',
});

const externalGauge = new client.Gauge({
  name: 'nodejs_external_bytes',
  help: '外部内存使用量(字节)',
});

// 定期更新指标
setInterval(() => {
  const used = process.memoryUsage();
  heapUsedGauge.set(used.heapUsed);
  heapTotalGauge.set(used.heapTotal);
  rssGauge.set(used.rss);
  externalGauge.set(used.external);
}, 5000);

// 导出端点
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.send(await client.register.metrics());
});

常见问题解答

Q1: 如何确定 Node.js 应用的内存需求?

A: 通过以下步骤确定:

javascript
// 1. 监控正常负载下的内存使用
const baseline = process.memoryUsage().heapUsed;

// 2. 压力测试下监控峰值
// 使用 ab、wrk 或 artillery 进行压力测试

// 3. 计算安全值
// 推荐配置 = 峰值使用量 × 1.5 + 缓冲区(200-500MB)

// 4. 留意内存增长趋势
// 使用 clinic.js 或自定义监控观察内存曲线

Q2: 如何快速定位内存泄漏?

A: 定位流程:

code
1. 确认泄漏
   - 监控内存使用是否持续增长
   - 手动 GC 后内存仍不下降

2. 生成堆快照
   - node --expose-gc --inspect app.js
   - 使用 Chrome DevTools 生成快照

3. 对比分析
   - 生成两个时间点的快照
   - 使用 Comparison 视图找出增长的对象

4. 查看引用链
   - 找到增长的对象
   - 查看 Retainers 了解为何无法释放

5. 检查常见问题
   - 全局变量
   - 未清除的定时器
   - 事件监听器
   - 闭包引用

Q3: --max-old-space-size 设置多大合适?

A: 建议规则:

bash
# 一般原则:不超过系统内存的 75%

# 示例:4GB 内存服务器
# 推荐设置: 3GB
node --max-old-space-size=3072 app.js

# 容器环境:考虑容器内存限制
# 容器限制 1GB -> 设置 768MB
node --max-old-space-size=768 app.js

# 注意:留出内存给
# - 操作系统
# - 其他进程
# - Buffer/外部内存
# - 突发增长

Q4: 如何处理大文件而不占用过多内存?

A: 使用流式处理:

javascript
// 方案 1:使用 Stream
const { pipeline } = require('stream');
const fs = require('fs');

pipeline(
  fs.createReadStream('large-file.txt'),
  transformStream, // 自定义转换
  fs.createWriteStream('output.txt'),
  (err) => { if (err) console.error(err); }
);

// 方案 2:逐行处理
const readline = require('readline');

async function processFile() {
  const stream = fs.createReadStream('large-file.txt');
  const rl = readline.createInterface({ input: stream });
  
  for await (const line of rl) {
    await processLine(line); // 逐行处理
  }
}

// 方案 3:分批处理
async function processBatch(items, batchSize = 100) {
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    await Promise.all(batch.map(processItem));
  }
}

Q5: Buffer 和 String 如何选择?

A: 选择建议:

场景推荐类型原因
网络 I/OBuffer避免编码转换开销
文件 I/OBuffer直接读写,性能更好
文本处理String编码方便,API 丰富
大量数据Buffer内存占用更可控
加密操作Buffer原生支持
javascript
// Buffer 适合 I/O 密集型
const buffer = Buffer.from('hello');
fs.writeFileSync('file.bin', buffer);

// String 适合文本处理
const text = 'hello world';
const processed = text.toUpperCase().split(' ');

最佳实践

开发阶段

  1. 代码审查

    • 检查全局变量使用
    • 确认定时器和监听器清理
    • 审查闭包引用
  2. 单元测试

    javascript
    // 测试内存泄漏
    function testMemoryLeak() {
      const before = process.memoryUsage().heapUsed;
      
      // 执行可能泄漏的操作
      for (let i = 0; i < 10000; i++) {
        const obj = createObject();
        // 确保对象可以被释放
      }
      
      // 强制 GC
      if (global.gc) global.gc();
      
      const after = process.memoryUsage().heapUsed;
      const growth = (after - before) / 1024 / 1024;
      
      console.log(`内存增长: ${growth.toFixed(2)}MB`);
      // 增长不应超过预期值
    }
  3. 性能分析

    bash
    # 使用 clinic.js 分析
    clinic heapprofiler -- node app.js
    
    # 使用 0x 分析 CPU 和内存
    0x app.js

生产阶段

  1. 监控设置

    • 配置内存告警阈值
    • 定期生成内存快照
    • 记录内存使用趋势
  2. 容器配置

    yaml
    # docker-compose.yml
    services:
      app:
        image: node:18
        deploy:
          resources:
            limits:
              memory: 2G
            reservations:
              memory: 1G
        environment:
          - NODE_OPTIONS=--max-old-space-size=1536
  3. 优雅重启

    javascript
    // 内存过高时自动重启
    const RESTART_THRESHOLD = 1000; // MB
    
    setInterval(() => {
      const used = process.memoryUsage();
      const heapUsedMB = used.heapUsed / 1024 / 1024;
      
      if (heapUsedMB > RESTART_THRESHOLD) {
        console.log('内存超限,准备优雅重启');
        gracefulShutdown().then(() => process.exit(1));
      }
    }, 60000);

性能优化检查清单

  • 避免使用全局变量存储大量数据
  • 及时清除定时器和事件监听器
  • 使用 Stream 处理大文件
  • 使用对象池复用频繁创建的对象
  • 使用 WeakMap/WeakSet 存储对象关联数据
  • 设置合理的内存限制参数
  • 实现内存监控和告警
  • 定期进行内存泄漏检测
  • 使用 LRU 缓存限制缓存大小
  • 避免在循环中创建大量临时对象
  • 正确处理 Promise 错误
  • 优化数据结构选择

参考资源