Java内存泄漏排查实践
Java内存泄漏排查实践Java 堆内存泄漏指对象本可被 GC 回收却因仍被GC Root 强引用住而无法释放导致堆 Used 持续上升最终OutOfMemoryError: Java heap space。排查主线是先确认是不是堆泄漏 → 再定位谁占着内存 → 最后看引用链为何删不掉。G1、JNI 等只在「现象对照」里点到为止不展开机制教学。速览典型信号老年代 Used 只升不降Full GC / Mixed GC 后回收很少内存曲线「阶梯上升」。先观测jstat -gcutil、GC 日志、监控jvm_memory_used_bytes。再取证jmap生成hprof→Eclipse MAT看 Leak Suspects / Dominator / GC Roots。易漏点无界 Map/Cache、ThreadLocal 未 remove、监听器未注销、资源未关闭。堆外Heap 不涨但进程 RSS 涨 → 可能是JNI / DirectByteBuffer等需另查 NMT不是本篇主线。目录一、概念与辨认1. 什么是堆内存泄漏2. 泄漏 vs 非泄漏3. 堆泄漏 vs 堆外泄漏含 JNI二、排查手段4. 线上观测指标与命令5. GC 日志与内存曲线6. 生成堆快照 hprof7. 用 MAT 分析 hprof三、模式与流程8. 常见泄漏模式9. 推荐排查顺序10. 速查卡1. 什么是堆内存泄漏概念说明泄漏对象业务上已不再使用但仍有引用链连到 GC Root表现堆Used单调涨或阶梯涨GC 后降幅越来越小终点java.lang.OutOfMemoryError: Java heap space正常请求结束 → 临时对象无引用 → Young GC 回收 → Used 回落锯齿 泄漏对象被 static Map / ThreadLocal 等挂住 → 每次 GC 都还在 → Old 越堆越多Retained HeapMAT 里 若该对象消失连带能释放的堆总量——分析泄漏时优先看这个而不是对象自身大小Shallow Heap。2. 泄漏 vs 非泄漏很多「内存一直很高」不是泄漏先排除再 dump。2.1 更像泄漏现象含义Old / 堆 Used只升不降GC 收不回来Full GC 后 Old 仍很高强引用常驻运行越久越慢最终 OOM经典时间维泄漏重启后正常跑几天又恶化与业务量/时间相关某类业务对象实例数随 QPS线性涨集合未清理2.2 常见「假泄漏」现象可能原因启动后内存持续上涨一段时间冷启动预热类加载、Bean、连接池、JIT中型 Spring 服务常见515 分钟才进入平台期Used 高但稳定有界缓存、单例、连接池——设计如此Old GC 多不一定是泄漏堆偏小、大对象多、晋升快等见下节一句对照Committed 大、Used 不涨JVM 已向 OS 预留堆不等于泄漏与 GC 现象的一句对照不展开 G1 原理健康应用多为Young GC 频繁、Old/Mixed GC 偶发若长期Old/Mixed GC 很密且回收后 Used 仍下不来应按泄漏优先排查而非理解为「Old 本来就要经常 GC」。2.3 内存曲线怎么读锯齿升→降→升 → 通常正常 阶梯上升每波更高 → 可疑建议 dump 对比 近似直线向上 → 高概率泄漏3. 堆泄漏 vs 堆外泄漏含 JNI本篇主线是Java 堆Heap。若堆指标正常但进程RSS常驻内存一直涨要怀疑堆外 / Native路径。是否是否内存持续上涨?Java Heap Used 涨?堆泄漏: hprof MATRSS 涨?堆外: NMT / pmap / 查 JNI·DirectBuffer可能非内存或监控口径问题现象优先方向Old 涨GC 回收不掉Java 堆泄漏→ 本文流程Heap Used 稳定RSS 涨JNI / Native、DirectMemory、线程栈等OutOfMemoryError: Direct buffer memory堆外 DirectByteBufferOutOfMemoryError: unable to create native thread线程 / 非堆JNI 相关泄漏仅列类型不展开写法JNI局部/全局引用未释放 → 对应 Java 对象无法回收堆上可能异常、Old GC 频繁。ByteBuffer.allocateDirect等堆外内存未释放 → Heap 不大RSS 很大。本地 C/Cmalloc未free→ RSS 涨MAT 看不出。堆外粗查jcmd pid VM.native_memory summary需启动参数-XX:NativeMemoryTrackingsummary。Java 堆泄漏仍以 hprof MAT 为准。4. 线上观测指标与命令4.1 jstat最快jstat-gcutilpid1000列关注泄漏时OOld是否持续升高FGC / GFGCFull GC 是否变密FGCTFull GC 总耗时是否飙升O 列绝对值不重要Old 60% 未必有问题是否随时间单调上升、Full GC 后是否回落才是关键。jstat-gcpid1000结合OUOld Used与OCOld Capacity看老年代是否顶满。4.2 jcmd 堆概况jcmdpidGC.heap_info关注committed … # JVM 已向 OS 申请的堆 used … # 对象实际占用关系Used ≤ Committed ≤ Max(Xmx)。Committed 大、Used 稳定→ 未必泄漏Used 持续涨→ 才重点查。4.3 监控指标Prometheus / Micrometer指标用途jvm_memory_used_bytes{areaheap}堆 Used 趋势jvm_memory_committed_bytes{areaheap}是否触顶扩容jvm_gc_pause_secondsGC 停顿进程 RSS与 Heap 对照判断堆外4.4 快速看对象分布jmap-histo:livepid|head-30关注实例数异常多的HashMap$Node、byte[]、String、业务 DTO 等。仅作线索定案仍需hprof MAT。5. GC 日志与内存曲线5.1 建议开启JDK 9-Xlog:gc*,gcheapdebug:filegc.log:time,uptime,level,tags -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/var/log/app/heap-oom.hprofOOM 时自动留hprof避免事后无现场。5.2 日志里看什么关注点泄漏倾向Full GC / Mixed GC 后Old 回收比例多次 10% → 高度可疑GC 后 Old Used是否一次比一次高阶梯上升 → 可疑仅启动阶段 GC 密集稳定后好转可能预热对比曲线时间轴不必死记 GC 算法名称看「GC 之后内存有没有真正掉下来」即可。6. 生成堆快照 hprof.hprof 某一时刻整个 Java 堆的对象快照含引用关系是 MAT 分析的输入。6.1 手动 dump低峰操作jmap -dump:live,formatb,fileheap-$(date%Y%m%d-%H%M).hprofpidlive先触发 Full GC再 dump仍被引用的对象查泄漏常用。会STW生产选低峰文件大小通常接近当前堆 Committed。若 dump 会导致过长 STW可优先等OOM 自动 dump容器环境需预留足够磁盘dump 体积 ≈ 当前堆 Committed。6.2 OOM 自动 dump强烈推荐-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/to/heap.hprof6.3 其他方式方式说明Arthasheapdump /tmp/heap.hprofVisualVM / JMX图形界面触发对比两次 dump间隔 30min数小时看谁一直在涨7. 用 MAT 分析 hprof工具Eclipse MATMemory Analyzer。大堆 dump 需调大MemoryAnalyzer.ini的-Xmx建议 ≥ dump 大小的 1.5 倍。打开File → Open Heap Dump → 选*.hprof→ 可选Leak Suspects Report。7.1 四步流程Leak SuspectsDominator TreeHistogramPath to GC Roots步骤操作目的1Reports →Leak Suspects自动给出嫌疑对象与占比2Dominator Tree按 Retained Heap 排序找「支配」大块内存的入口3Histogram按类过滤业务包名看哪类对象实例数异常4右键实例 →Path To GC Roots→ exclude weak/soft/phantom看谁强引用着它7.2 看到什么算「破案」GC Root 链末端常见原因static字段上的 Map/List静态集合只增不减Thread→ThreadLocalMapThreadLocal 未remove()线程池场景高发Spring 单例 Bean 持有大集合缓存未设上限/过期监听器注册未注销SDK / UI / 消息回调若 GC Root 链只到main/ Spring 容器 Bean且 Retained 与业务预期一致可能是正常单例或有界缓存不一定是泄漏与 2.2 呼应。7.3 MAT 不能回答的GC 为什么频繁看 GC 日志CPU 热点profilerJNI / 堆外泄漏看 NMT、RSS8. 常见泄漏模式模式典型代码/场景排查提示无界 Map/Cachestatic Map、Guava 无maximumSize/expireHistogram 里 Map 节点暴涨ThreadLocal 线程池set后未removePath to GC Roots 经 ThreadLocalMap监听器未注销注册回调、Netty/MQ 监听器单例或 static 持有资源未关闭Stream、Connection 被对象间接引用常伴随连接池/leak 检测Session / 请求上下文堆积全局 Map 以 sessionId 为 key 不删与在线用户数相关误用「缓存」以为有上限实际无过期Retained 大但业务称「缓存设计」——需产品确认是否泄漏9. 推荐排查顺序1. 确认现象 jstat / 监控Old、Heap Used 是否持续涨Full GC 后是否回收 2. 排除假泄漏 是否仍在冷启动窗口是否本来就有大缓存 3. 开 GC 日志 OOM 自动 dump 保留 gc.log配置 HeapDumpOnOutOfMemoryError 4. jmap -histo:live线索 看是否有异常突出的类 5. 低峰 jmap -dump:live → hprof 必要时间隔一段时间 dump 第二次对比 6. MATLeak Suspects → Dominator → Histogram → GC Roots 定位类 引用链 → 回到代码 7. 若 Heap 不涨、RSS 涨 转堆外 / JNI 路径NMT不在本篇展开阶段产出观测「是不是堆在漏」hprof「谁占着内存」MAT「为什么删不掉」修代码remove / 限流 / 过期 / 注销 / try-with-resources10. 速查卡┌─────────────────────────────────────────────────────────┐ │ 信号: Old/Heap Used 只升不降Full GC 后回收 10% │ │ 观测: jstat -gcutil | jcmd GC.heap_info | GC 日志 │ │ 取证: jmap -dump:live → heap.hprof → Eclipse MAT │ │ MAT: Leak Suspects → Dominator → Path to GC Roots │ │ 高发: static Map | ThreadLocal | 无界 Cache | 监听器 │ ├─────────────────────────────────────────────────────────┤ │ Heap 不涨、RSS 涨 → 查堆外/JNI/DirectBuffer非本篇主线│ └─────────────────────────────────────────────────────────┘建议 JVM 参数生产基线-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/var/log/app/heap-oom.hprof -Xlog:gc*:filegc.log:time,level,tags落地注意jmap -dump:live与jmap -histo:live都会触发 Full GC勿在高峰频繁执行。分析以Retained Heap为准Shallow 大可能是被别的大对象引用。修复后做压测 长时间观察Old/Used 曲线确认阶梯上升消失。堆外、JNI、本地库问题需NMT / 代码审查 / 原生工具不能单靠 MAT。本篇不适用Metaspace OOM、CodeCache、栈溢出StackOverflowError——需另查类加载、JIT、线程栈深度。纯堆外泄漏定位——需 NMT /pmap/ ASan 等见 第 3 节 分流。一句话Java 堆泄漏排查 Used 持续涨时抓hprof用MAT找Retained 最大对象和GC Root 引用链先排除预热与缓存设计再区分堆内MAT与堆外/JNIRSS NMT。