nodejs堆内存超出上限报错

Node.js JavaScript heap out of memory 错误的排查与解决

#status / growing #type / debug #resource / nodejs

[!info] related notes

nodejs堆内存超出上限报错

现象

Node.js 进程运行中或构建时崩溃,终端输出类似:

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

<--- Last few GCs --->
[12345:0x5f8a8c0]   123456 ms: Scavenge 1398.2 (1450.5) -> 1398.1 (1450.5) MB, 5.2 / 0.0 ms
[12345:0x5f8a8c0]   234567 ms: Mark-sweep 1450.1 (1450.5) -> 1449.8 (1451.0) MB, 100.5 / 0.0 ms

# 或 Worker 线程中
ERR_WORKER_OUT_OF_MEMORY

常见触发场景:

  • pnpm build / npm run build 大型 Monorepo 项目
  • 处理大文件(解析 JSON / CSV)
  • 递归过深的算法
  • 长时间运行的 Node.js 服务内存泄漏

原因

1. V8 堆内存默认上限

V8 引擎为 Node.js 进程分配的默认堆内存上限:

系统内存默认堆上限
< 2GB~1.5GB
>= 2GB~1.7GB
>= 4GB~4GB(Node >= 12)

当程序实际内存需求超过此限制时就会报错。

2. 内存泄漏(Memory Leak)

  • 全局变量持续累积
  • 事件监听器未正确移除
  • 闭包引用了大对象
  • 定时器未清理

3. 大数据一次性加载

将整个文件读入内存处理,而非使用流式处理。

4. 构建工具并发过高

Monorepo 中多个包同时构建,内存叠加超过上限。

排查过程

1. 确认内存使用情况

# 运行时观察内存
node --trace-gc app.js

# 输出示例:
# [12345] 12345 ms: Scavenge 50.2 (60.5) -> 49.8 (62.0) MB

2. 使用 Chrome DevTools 远程调试

# 启动时开启 inspect
node --inspect app.js
# 或指定端口
node --inspect=9229 app.js

# 构建工具
NODE_OPTIONS="--inspect" pnpm build

打开 Chrome 浏览器访问 chrome://inspect,连接到目标进程。

3. 抓取 Heap Snapshot

在 Chrome DevTools → Memory 面板:

  1. 点击 “Take heap snapshot”
  2. 执行一段时间操作后再拍一次
  3. 对比两个 snapshot,查找持续增长的对象

4. 使用 —expose-gc 手动触发 GC

// 手动触发垃圾回收并观察内存
global.gc()
console.log(process.memoryUsage())

// 输出:
// {
//   rss: 50000000,      - 进程占用的物理内存
//   heapTotal: 30000000, - V8 堆总量
//   heapUsed: 20000000,  - V8 堆已用
//   external: 1000000    - C++ 对象绑定的内存
// }

5. 分析内存快照差异

在 Chrome DevTools Memory 面板中:

  • 选择 “Comparison” 视图
  • 对比两次 snapshot
  • 按 “Delta” 排序,找到增量最大的对象
  • 展开 Retainer chain 查看引用路径

解决方案

方案一:提升 Node.js 最大内存限制(最直接)

单次执行

NODE_OPTIONS="--max-old-space-size=4096" pnpm build

写入 Shell 配置(持久化)

# ~/.bashrc 或 ~/.zshrc
export NODE_OPTIONS="--max-old-space-size=4096"

Windows PowerShell

$env:NODE_OPTIONS="--max-old-space-size=4096"
pnpm build

[!warning] 注意 --max-old-space-size 不能超过系统物理内存。设置过大反而会导致系统 swap,性能更差。

方案二:限制构建工具并发数(针对 Monorepo)

# pnpm - 限制并发
pnpm build --concurrency 2

# nx - 限制并行
nx run-many --target=build --parallel=2

# turbo - 限制并发
turbo build --concurrency=2

方案三:流式处理替代全量加载

// 错误:一次性读入内存
const data = JSON.parse(fs.readFileSync('huge.json', 'utf-8'))

// 正确:流式处理
const { createReadStream } = require('fs')
const { parser } = require('stream-json')
const { streamArray } = require('stream-json/streamers/StreamArray')

createReadStream('huge.json')
  .pipe(parser())
  .pipe(streamArray())
  .on('data', ({ value }) => {
    // 逐条处理,不占大量内存
    processItem(value)
  })

方案四:Worker Threads 分担内存压力

const { Worker } = require('worker_threads')

// 将密集计算分配到独立 Worker,每个 Worker 有独立的堆内存
const worker = new Worker('./heavy-task.js', {
  workerData: { chunk: dataChunk },
})

worker.on('message', (result) => {
  // 处理结果
})

worker.on('error', (err) => {
  console.error('Worker error:', err)
})

方案五:修复内存泄漏

// 检查事件监听器泄漏
process.on('warning', (warning) => {
  if (warning.name === 'MaxListenersExceededWarning') {
    console.error('Possible memory leak:', warning)
  }
})

// 使用 WeakRef / WeakMap 避免强引用
const cache = new WeakMap()
cache.set(largeObject, metadata)
// largeObject 被 GC 回收时,缓存自动清理

回归验证

  1. 构建验证:执行 NODE_OPTIONS="--max-old-space-size=4096" pnpm build,确认构建成功完成
  2. 内存监控:使用 --trace-gc 观察 GC 后堆内存是否稳定,不再持续增长
  3. 压力测试:使用 autocannonwrk 对服务施压,确认高负载下内存稳定
  4. 长时间运行:服务运行 24 小时后检查 process.memoryUsage(),确认无持续增长
  5. 回归测试:确认相关功能正常,无因内存限制调整导致的副作用

相关笔记

创建于 2026/2/26 更新于 2026/5/27