怎么排查内存泄漏?

1 个回答

  • 玄差
    没觉得一年比一年自由,但确实一年比一年通透。
    # 0. 先判断:是不是“真泄漏”? 很多“泄漏”其实是**缓存预热、流量增长、未调 GC、限额太紧**。 * **现象对比** * 真泄漏:进程常驻内存(RSS/堆使用)**锯齿上升且不回落**,直到 OOM。 * 非泄漏:**锯齿上下波动**,有回落到稳定平台。 * **快速排查** * 看容器/主机内存图(Prometheus/Grafana/Top/`kubectl top`)。 * 打开 GC 日志(JDK9+ 默认易开):`-Xlog:gc*:file=gc.log:tags,uptime,level`。 * 观察 Young/Old 使用量是否在 Full GC 后仍显著升高。 # 1. 线上“先活命”:限流+内存保护 * **临时降载**(限流/摘流),避免系统持续放大泄漏。 * JVM 保护参数(Java 8+ 建议): * `-XX:+ExitOnOutOfMemoryError`(OOM 直接退出,避免僵尸进程) * 容器内:`-XX:+UseContainerSupport -XX:MaxRAMPercentage=60`(别把 cgroup 内存打满) # 2. 取证:拿“时序样本”而不是单点 **同一进程拿 2\~3 份间隔 5–10 分钟的堆快照**,方便做对比(diff),找“谁在长”。 * 开启安全的 dump 方式(不会停很久): * JDK8:`jmap -dump:live,format=b,file=/tmp/heap1.hprof <pid>` * JDK9+:`jcmd <pid> GC.heap_dump filename=/tmp/heap1.hprof` * 同时取 **线程栈**(关联业务调用):`jstack <pid> > /tmp/th1.txt` > **live** 只保留可达对象,减少噪音。若 dump 失败或过慢,先抓 **直方图**: > `jcmd <pid> GC.class_histogram > /tmp/histo1.txt`(几十毫秒级) # 3. 分析堆:Dominator Tree + 引用链 用 **Eclipse MAT** / **VisualVM** / **YourKit** 打开 hprof: 1. 看 **Dominator Tree(支配树)**:Top Retained Size 最大的类/对象是谁。 2. “Leak Suspects Report”:MAT 自动生成可疑增长路径。 3. 选中可疑对象 → **Path To GC Roots**(带排除软/弱引用)查看**强引用链**。 4. **对比快照(heap1 vs heap2)**:看哪些类实例数/保留大小持续增长。 > 典型可疑项:`java.util.*` 集合(Map/List/Queue)尺寸持续变大、`ThreadLocalMap`、自定义缓存、监听器列表、Guava/Caffeine 缓存不过期。 # 4. 结合线程/业务:把对象增长对应到代码 * 用堆中对象的**字段/栈轨迹**定位来源(许多 profiler 会给出**分配栈**)。 * 对应 `jstack` 查找长时间运行/阻塞的业务线程、定时任务、消费者线程。 * Web 应用看**请求维度**:哪些接口调用后对象不释放(APM:SkyWalking/Pinpoint/Arthas trace)。 # 5. 常见“真凶”与定位方法(对号入座) 1. **静态集合 / 单例缓存**未过期 * 现象:`HashMap/ConcurrentHashMap` Retained Size 巨大。 * 处理:加 TTL/最大容量(Caffeine/Guava),或 `Weak/SoftReference`。 2. **监听器/回调未注销** * 现象:Observer 列表持有大量业务对象。 * 处理:在销毁/完成时 `removeListener`。 3. **ThreadLocal 未 remove(线程池复用)** * 现象:`ThreadLocalMap` 中 Value 类对象堆积。 * 处理:用完 `remove()`;尽量不在池线程放大对象生命周期。 4. **不合理的缓存 Key**(高基数、未命中导致“雪崩式膨胀”) * 现象:缓存 map key 数持续增加。 * 处理:统一 Key 规范、限容、BloomFilter、热点隔离。 5. **类加载器泄漏(热部署/插件)** * 现象:多个 `WebAppClassLoader` 常驻、`byte[]` 类字节码留存。 * 处理:确保所有线程/Timer/驱动在卸载前释放;避免 `static` 持老 ClassLoader。 6. **IO/连接对象未关闭**(直观上是资源泄漏,可能连带堆对象常驻) * 处理:`try-with-resources`;连接池正确归还。 7. **Off-heap / 直接内存**(Netty/DirectByteBuffer) * 现象:堆占用不高但进程 RSS 不断上涨。 * 定位: * `-XX:MaxDirectMemorySize=` 合理设置; * **NMT**:`-XX:NativeMemoryTracking=summary`,`jcmd <pid> VM.native_memory summary` 看直接内存、线程栈、GC、类元空间占用; * Netty 开启泄漏检测:`-Dio.netty.leakDetection.level=paranoid`。 8. **Metaspace 脚手架**(动态代理/反射生成类过多) * 现象:Full GC 也不回落,`OutOfMemoryError: Metaspace`。 * 定位:`jcmd VM.native_memory` 看 `Metaspace`;检查字节码生成框架使用。 # 6. 代码级防漏清单(上线前自检) * **集合**:所有自研缓存必须有 **容量上限 + 过期策略**。 * **ThreadLocal**:使用 try/finally `remove()`;不要存大对象;必要时封装成“自动清理”的工具。 * **监听/订阅**:注册必有注销;使用弱引用监听器(`WeakReference`/`WeakHashMap`)视场景。 * **连接与句柄**:`try-with-resources` 必开;线程池/定时器 `shutdown()`。 * **对象生命周期**:尽量缩短;避免把请求作用域对象放到单例/静态。 * **监控**:打点导出 **Heap/Old/Metaspace/Direct** 使用量、GC 次数与停顿,设告警阈值。 # 7. 线上零停机定位(可选高级手段) * **Arthas**(阿里开源):`heapdump`, `stack`, `watch`,在线观察对象与方法入参/返回。 * **Async-profiler**:火焰图看**分配热点**(`-e alloc`),找“谁在疯狂 new”。 * **BTrace / Byteman**:对可疑点注入采样日志。 # 8. 最小闭环流程(8 步记忆法) > 观测 → 活命 → 多次 dump → 支配树 → 引用链 → 结合线程/业务 → 修复上线 → 验证回落 ## 常用命令速查 ```bash # 进程与端口 jps -l # 堆直方图(快) jcmd <pid> GC.class_histogram > /tmp/histo.txt # 堆转储(live) jcmd <pid> GC.heap_dump filename=/tmp/heap.hprof # 线程栈 jstack <pid> > /tmp/th.txt # 原生内存(NMT) jcmd <pid> VM.native_memory summary # GC 日志(JDK8) -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc.log ``` ### 一句话总结 > 判断真泄漏 → 多份堆快照做 **diff** → 用 **Dominator Tree + GC Roots** 找增长对象与引用链 → 结合线程/业务定位代码 → 修复典型坑(静态集合、ThreadLocal、监听器、缓存)→ 覆盖 **堆/堆外/元空间** 全面验证。
    发布于 2025-08-13 15:46 •IP属地 山西
    举报
    评论
    收藏