内存泄露猎手
找出哪些因素导致应用程序内存的消耗。分析堆增长模式,比较快照,识别保留的对象(事件监听器、闭包、定时器、断开的DOM节点、未关闭的连接),并生成特定的修复方案——不仅仅是“存在内存泄露”,而是确切地指出哪里和为什么。
使用时:当出现“内存泄露”、“内存持续增长”、“OOM杀死”、“堆转储分析”、“为什么内存使用量这么高”、“应用程序内存持续增加”,或内存使用量在稳定的流量下无法稳定时。
命令
步骤1:测量内存增长
# Node.js — 跟踪堆内存随时间变化
node -e " const used = () => process.memoryUsage(); setInterval(() => { const m = used(); console.log(JSON.stringify({ rss: (m.rss / 1048576).toFixed(1) + ' MB', heap: (m.heapUsed / 1048576).toFixed(1) + ' MB', external: (m.external / 1048576).toFixed(1) + ' MB' })); }, 5000); " &
# 发送负载2分钟,然后观察内存是否返回到基线
# Python — 跟踪内存
python3 -c " import tracemalloc, time tracemalloc.start() # ... 运行您的代码 ... snapshot = tracemalloc.take_snapshot() for stat in snapshot.statistics('lineno')[:10]: print(stat) "
# 任何进程 — 监视RSS随时间变化
watch -n 5 "ps -o pid,rss,vsz,comm -p \$(pgrep -f 'your-app')"
# 容器内存
kubectl top pods --sort-by=memory 2>/dev/null
docker stats --no-stream 2>/dev/null
步骤2:堆快照分析(Node.js)
# 在间隔时间内获取堆快照
# 方法1:通过检查器协议
node --inspect=9229 app.js &
# 发送信号获取快照
kill -USR2 $(pgrep -f app.js)
# 方法2:程序化
node -e " const v8 = require('v8'); const fs = require('fs'); const snap1 = v8.writeHeapSnapshot(); console.log('Snapshot 1:', snap1); // ... 执行工作 ... const snap2 = v8.writeHeapSnapshot(); console.log('Snapshot 2:', snap2); "
比较两个快照 —— 之间增长的对象是泄露候选者:
按保留大小增量排序
关注计数增加但无对应减少的对象
查找意外的字符串保留(通常是日志缓冲区或错误消息)
步骤3:常见泄露模式
事件监听器未移除:
rg "addEventListener|\.on\(|\.once\(" --type ts --type js -g '!node_modules' 2>/dev/null | \
while read line; do file=$(echo "$line" | cut -d: -f1) # 检查是否有对应的removeEventListener
rg "removeEventListener|\.off\(|\.removeListener\(" "$file" 2>/dev/null | wc -l
done
闭包保留作用域:
# 找到循环或重复回调中捕获的大对象的闭包
rg -U "setInterval|setTimeout|\.on\(.
=>\s\{" --type ts --type js -g '!node_modules' 2>/dev/null | head -20
未关闭的资源:
# 找到打开但没有关闭的资源
rg "\.open\(|createConnection|createPool|createClient" --type ts --type js --type py -g '!node_modules' 2>/dev/null
# 检查同一文件中的.close()或.end()或.destroy()
断开的DOM节点(浏览器):
# 找到没有清理的DOM操作
rg "createElement|appendChild|insertBefore" --type ts --type js -g '!node_modules' 2>/dev/null
# 检查是否有对应的removeChild或清理
增长的集合:
# 找到只推送/添加但从不清除/删除的数组/映射/集合
rg "\.push\(|\.set\(|\.add\(" --type ts --type js -g '!node_modules' -g '!
.test.' 2>/dev/null | head -20
步骤4:生成报告
# 内存泄露分析
证据
- RSS从120 MB增长到850 MB,6小时内稳定负载
- 堆使用从80 MB增长到620 MB
- 没有相应的活动连接或请求增加
- 内存在流量停止后不减少 → 确认泄露
泄露的对象(从堆差异中)
- 字符串 — +45,000个实例,+120 MB保留
来源:
logger.ts:23 — 错误消息追加到内存缓冲区中,没有轮换
修复:添加最大缓冲区大小或使用流式日志记录器
来源:
websocket.ts:67 —
socket.on('message')处理器在重新连接时添加,从不移除
修复:在重新添加之前调用
socket.removeAllListeners('message')
来源:
api-client.ts:45 — 超时Promise创建但从不在断开连接时拒绝
修复:添加AbortController并在超时时清理
推荐
- 将
--max-old-space-size=512添加到Node.js标志中作为安全限制
- 实现日志轮换(内存缓冲区中最多1000个条目)
- 在WebSocket重新连接处理器中添加连接清理
- 监视
process.memoryUsage()并在达到限制的70%时发出警报
- profile — 连续内存分析
设置轻量级连续内存监视:
周期性堆统计日志记录(每30秒,低开销)
警报阈值(绝对MB,增长率MB/分钟)
在OOM即将发生时自动堆快照
与APM工具集成(Datadog,New Relic,Prometheus)
对于每个识别的泄露模式,提供确切的代码修复:
事件监听器清理在组件生命周期中
WeakMap/WeakRef用于缓存