内存泄漏
用堆快照和保留路径分析检测并修内存泄漏,防 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 崩非内核杀。