← 返回题目列表

Node.js 服务内存泄漏如何排查?

困难 第 24 / 27 题 更新于 2026/07/29
Node.js内存泄漏V8Heap Snapshot

简化版

Node.js 内存泄漏排查要先确认 RSS、heapUsed 是否持续上涨,再结合 heap snapshot、allocation profile、GC 日志和压测复现定位保留对象。常见原因包括全局缓存无限增长、事件监听未移除、闭包引用大对象、定时器未清理和请求上下文持有过多数据。

详细版

先用指标判断是不是泄漏,而不是看到内存高就下结论。

setInterval(() => {
  const m = process.memoryUsage()
  console.log({
    rss: m.rss,
    heapUsed: m.heapUsed,
    external: m.external
  })
}, 10_000)

如果 GC 后 heap 仍持续上升,并且请求停止后不回落,就要抓 heap snapshot 对比对象增长。排查重点是“谁还引用着这些对象”,而不是只看对象大小。

完整版教学

一、先区分内存高和内存泄漏

内存高不一定是泄漏。 缓存、连接池、大批量任务、V8 堆扩容都会让内存上升。 泄漏的关键特征是:业务负载结束后,本该释放的对象仍被引用,内存长期不回落。

正常缓存:
内存上升 -> 达到稳定平台 -> 波动

疑似泄漏:
内存上升 -> GC 后仍上升 -> 请求停止也不回落

假设压测 30 分钟,heapUsed 从 200 MB 到 800 MB,停止压测后仍维持 780 MB。 这比单次看到 800 MB 更能说明问题。

二、理解几个关键内存指标

process.memoryUsage() 能看到 RSS、heapTotal、heapUsed、external、arrayBuffers。 V8 堆主要看 heapUsed。 Buffer、原生扩展和部分外部内存会体现在 external 或 RSS 上。

const {
  rss,
  heapTotal,
  heapUsed,
  external,
  arrayBuffers
} = process.memoryUsage()
指标含义排查方向
rss进程占用物理内存总体压力
heapUsedV8 堆已用JS 对象泄漏
externalV8 外部内存Buffer/native
arrayBuffersArrayBuffer/Buffer大二进制数据

如果 heapUsed 稳定但 RSS 飙升,问题可能不在普通 JS 对象。 可能是 Buffer、大文件处理、原生模块或内存碎片。

三、heap snapshot 看的是引用链

Heap Snapshot 能展示堆里的对象、大小和引用关系。 排查泄漏时最重要的是 retained size 和 retaining path。 你要找出为什么对象没有被 GC 回收。

LeakedUserSession
<- Map cache
<- global sessionStore
<- module scope

这个引用链说明对象被模块级缓存 Map 持有。 即使请求结束,只要 Map 不删,它就不会被回收。 所以泄漏排查的本质是找“谁还拉着它不放手”。

四、对比快照比单张快照更有价值

单张快照只能告诉你当前有什么。 对比快照能告诉你两次之间增长了什么。 常见做法是在压测前抓一次,压测一段时间抓一次,停止流量并触发 GC 后再抓一次。

Snapshot A: 压测前
Snapshot B: 压测中
Snapshot C: 停止后

重点看 B 到 C 仍未释放的对象

如果某类对象从 1 万个增长到 30 万个,并且停止后仍不下降,它就是重点嫌疑。 数量增长有时比单个对象大小更危险。

五、常见泄漏来源很固定

Node 服务常见泄漏大多不是神秘问题。 全局数组缓存、Map 不淘汰、EventEmitter 监听不移除、定时器不清理、闭包持有请求对象、AsyncLocalStorage 存大对象,都很常见。

// 危险:无限增长缓存
const cache = new Map()

app.get('/user/:id', async (req, res) => {
  cache.set(req.params.id, await loadUser(req.params.id))
})
来源表现修复
无限缓存Map/数组持续增长TTL/LRU/上限
监听未移除listener 增多cleanup
定时器未清理对象被回调引用clearInterval
大 Bufferexternal 增长流式处理

如果缓存没有上限,在高基数用户 ID 场景里迟早变成泄漏。 缓存一定要有容量边界。

六、生产排查要控制风险

抓 heap snapshot 可能暂停进程并占用额外内存。 生产环境不要在满负载主实例上随便抓。 更安全的方式是灰度实例、复现环境、诊断端口受控开放,或者在容器副本中复现。

记忆钩子:内存泄漏不是对象大,而是对象该走没走;看引用链,找谁拽着它。

排查时先指标、再复现、再快照、再引用链。 不要一上来调大 --max-old-space-size,那只是把爆炸时间往后推。

七、常见误区与追问

  • 误区:内存占用高就是泄漏。 缓存、连接池和 V8 堆扩容都可能让内存高,要看 GC 后是否持续不回落。
  • 误区:调大 max-old-space-size 就解决了。 这只扩大堆上限,真正泄漏仍会继续增长。
  • 误区:只看 shallow size 就能定位。 retained size 和引用链更关键,因为它解释对象为什么释放不了。
  • 追问:heapUsed 和 rss 有什么区别? heapUsed 是 V8 JS 堆,RSS 是进程总物理内存,占用范围更大。
  • 追问:Buffer 泄漏看哪个指标? 重点看 external、arrayBuffers 和 RSS。
  • 追问:EventEmitter 泄漏怎么发现? listener 数量持续增长,可能出现 MaxListenersExceededWarning,也可从快照看监听器引用链。

八、加强记忆

内存泄漏排查按“四步走”:看指标趋势,压测复现,对比快照,追引用链。常见元凶是无限缓存、未清理监听、未清理定时器、大 Buffer 和闭包引用。别急着加内存,先找谁没放手。