Skip to content
返回构建教程

内存泄漏

用堆快照和保留路径分析检测并修内存泄漏,防 prod OOM。

为什么

长跑服务器的内存泄漏是定时炸弹:dev 好(每 5 分钟重启)过每测试,prod 凌晨 4 点 OOM 无线索。泄漏几乎总是保留引用——忘删的事件监听、捕获大对象的闭包、无界增长的 Map——找到后修是一行。检测法:取两堆快照、diff、检保留对象。Chrome DevTools 和 Node --inspect 5 分钟做;无它你从'process.memoryUsage() 涨了'猜。跨部署存活的泄漏是最差 bug——崩前不可见,崩无栈追踪。

何时使用

长跑进程内存单调增长时。找+修约 1-2 小时。退出的 CLI 工具可跳过。

如何做

1. 确认是泄漏——内存涨且 GC 后不降
   - 每 60s 记 process.memoryUsage().heapUsed;30 分钟趋势上则是泄漏
   - --expose-gc + global.gc() 强 GC 排除"只是未回收"

2. 负载下取两堆快照隔 5-10 分钟
   - Node:inspector.open();然后 chrome://inspect → Memory → Take heap snapshot
   - 或编程:v8.writeHeapSnapshot()

3. diff 快照按"delta 保留大小"排
   - 快照间增长的对象是泄漏
   - 看保留路径:谁持引用?

4. 常见泄漏模式——逐个对 diff 查
   - 事件监听未删(emitter.on 无 emitter.off)
   - Map/Set 无界增长(无驱逐缓存)
   - 闭包无意捕获大对象
   - 定时器(setInterval)未清

5. 修保留,1 小时 soak 测试验证
   - 内存应平非涨;重快照确认 diff 现空

示例

// 确认泄漏——随时间记堆
setInterval(() => {
  const m = process.memoryUsage();
  console.log(JSON.stringify({ t: Date.now(), heap: m.heapUsed, rss: m.rss }));
}, 60_000);
// 30 分钟趋势上 → 泄漏

// 编程取堆快照
import { writeHeapSnapshot } from "node:v8";
app.post("/debug/snapshot", (req, res) => {
  const file = writeHeapSnapshot();
  res.json({ file });  // 下载 chrome://inspect diff
});

// 常见泄漏:监听未删
function onConnect(socket) {
  emitter.on("data", (d) => socket.write(d));   // ← 泄漏:socket 关但监听留
  socket.on("close", () => emitter.off("data", ???));  // 需同函数引用!
}
// 修:命名 handler 使 off() 可删
function onConnect(socket) {
  const onData = (d) => socket.write(d);
  emitter.on("data", onData);
  socket.on("close", () => emitter.off("data", onData));
}

常见问题

  • ⚠责框架不查——10 次中 9 次是你代码持引用。
  • ⚠取一快照猜;需两快照 diff 看增长。
  • ⚠修泄漏不加 soak 测试,下重构回归。
  • ⚠需普通 Map 处用 WeakMap(或反)——修造新 bug。

最佳实践

  • ✓CI 每服务器 PR 跑 1 小时 soak——内存应平;单调涨失败构建。
  • ✓应 GC 键对象键的缓存用 WeakMap/WeakRef——键死缓存自驱逐。
  • ✓每内存缓存用 LRU 驱逐(如 lru-cache)封顶,失控不 OOM——按构造有界。
  • ✓设内存限(Node --max-old-space-size)低于容器限,得有用 OOM 崩非内核杀。