nodejs堆内存超出上限报错
Node.js JavaScript heap out of memory 错误的排查与解决
#status / growing
#type / debug
#resource / nodejs
[!info] related notes
- 所属 MOC: nodejs-moc
- 相关概念: [[buffer]], worker-threads
- 内存管理: ecmascript-memory-management
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 面板:
- 点击 “Take heap snapshot”
- 执行一段时间操作后再拍一次
- 对比两个 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 回收时,缓存自动清理
回归验证
- 构建验证:执行
NODE_OPTIONS="--max-old-space-size=4096" pnpm build,确认构建成功完成 - 内存监控:使用
--trace-gc观察 GC 后堆内存是否稳定,不再持续增长 - 压力测试:使用
autocannon或wrk对服务施压,确认高负载下内存稳定 - 长时间运行:服务运行 24 小时后检查
process.memoryUsage(),确认无持续增长 - 回归测试:确认相关功能正常,无因内存限制调整导致的副作用