news 2026/5/27 9:09:50

Java内存泄漏排查实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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生成hprofEclipse 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. 生成堆快照 hprof
  • 7. 用 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 Heap(MAT 里)= 若该对象消失,连带能释放的堆总量——分析泄漏时优先看这个,而不是对象自身大小(Shallow Heap)。


2. 泄漏 vs 非泄漏

很多「内存一直很高」不是泄漏,先排除再 dump。

2.1 更像泄漏

现象含义
Old / 堆 Used只升不降GC 收不回来
Full GC 后 Old 仍很高强引用常驻
运行越久越慢,最终 OOM经典时间维泄漏
重启后正常,跑几天又恶化与业务量/时间相关
某类业务对象实例数随 QPS线性涨集合未清理

2.2 常见「假泄漏」

现象可能原因
启动后内存持续上涨一段时间冷启动预热(类加载、Bean、连接池、JIT);中型 Spring 服务常见5~15 分钟才进入平台期
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 + MAT

RSS 涨?

堆外: NMT / pmap / 查 JNI·DirectBuffer

可能非内存或监控口径问题

现象优先方向
Old 涨,GC 回收不掉Java 堆泄漏→ 本文流程
Heap Used 稳定,RSS 涨JNI / Native、DirectMemory、线程栈等
OutOfMemoryError: Direct buffer memory堆外 DirectByteBuffer
OutOfMemoryError: unable to create native thread线程 / 非堆

JNI 相关泄漏(仅列类型,不展开写法)

  • JNI局部/全局引用未释放 → 对应 Java 对象无法回收,堆上可能异常、Old GC 频繁。
  • ByteBuffer.allocateDirect等堆外内存未释放 → Heap 不大,RSS 很大。
  • 本地 C/C++mallocfree→ RSS 涨,MAT 看不出

堆外粗查:jcmd <pid> VM.native_memory summary(需启动参数-XX:NativeMemoryTracking=summary)。Java 堆泄漏仍以 hprof + MAT 为准。


4. 线上观测:指标与命令

4.1 jstat(最快)

jstat-gcutil<pid>1000
关注泄漏时
O(Old)是否持续升高
FGC / GFGCFull GC 是否变密
FGCTFull GC 总耗时是否飙升

O 列:绝对值不重要(Old 60% 未必有问题),是否随时间单调上升、Full GC 后是否回落才是关键。

jstat-gc<pid>1000

结合OU(Old Used)OC(Old Capacity)看老年代是否顶满。

4.2 jcmd 堆概况

jcmd<pid>GC.heap_info

关注:

committed = … # JVM 已向 OS 申请的堆 used = … # 对象实际占用

关系:Used ≤ Committed ≤ Max(Xmx)
Committed 大、Used 稳定→ 未必泄漏;Used 持续涨→ 才重点查。

4.3 监控指标(Prometheus / Micrometer)

指标用途
jvm_memory_used_bytes{area="heap"}堆 Used 趋势
jvm_memory_committed_bytes{area="heap"}是否触顶扩容
jvm_gc_pause_secondsGC 停顿
进程 RSS与 Heap 对照,判断堆外

4.4 快速看对象分布

jmap-histo:live<pid>|head-30

关注实例数异常多的HashMap$Nodebyte[]String、业务 DTO 等。
仅作线索,定案仍需hprof + MAT


5. GC 日志与内存曲线

5.1 建议开启(JDK 9+)

-Xlog:gc*,gc+heap=debug:file=gc.log:time,uptime,level,tags -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/heap-oom.hprof

OOM 时自动留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,format=b,file=heap-$(date+%Y%m%d-%H%M).hprof<pid>
  • live:先触发 Full GC,再 dump仍被引用的对象(查泄漏常用)。
  • STW,生产选低峰;文件大小通常接近当前堆 Committed。

若 dump 会导致过长 STW,可优先等OOM 自动 dump;容器环境需预留足够磁盘(dump 体积 ≈ 当前堆 Committed)。

6.2 OOM 自动 dump(强烈推荐)

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heap.hprof

6.3 其他方式

方式说明
Arthasheapdump /tmp/heap.hprof
VisualVM / JMX图形界面触发
对比两次 dump间隔 30min~数小时,看谁一直在涨

7. 用 MAT 分析 hprof

工具:Eclipse MAT(Memory Analyzer)。大堆 dump 需调大MemoryAnalyzer.ini-Xmx(建议 ≥ dump 大小的 1.5 倍)。

打开:File → Open Heap Dump → 选*.hprof→ 可选Leak Suspects Report

7.1 四步流程

Leak Suspects

Dominator Tree

Histogram

Path 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静态集合只增不减
ThreadThreadLocalMapThreadLocal 未remove()(线程池场景高发)
Spring 单例 Bean 持有大集合缓存未设上限/过期
监听器注册未注销SDK / UI / 消息回调

若 GC Root 链只到main/ Spring 容器 Bean,且 Retained 与业务预期一致,可能是正常单例或有界缓存,不一定是泄漏(与 2.2 呼应)。

7.3 MAT 不能回答的

  • GC 为什么频繁(看 GC 日志)
  • CPU 热点(profiler)
  • JNI / 堆外泄漏(看 NMT、RSS)

8. 常见泄漏模式

模式典型代码/场景排查提示
无界 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. MAT:Leak Suspects → Dominator → Histogram → GC Roots 定位类 + 引用链 → 回到代码 7. 若 Heap 不涨、RSS 涨 转堆外 / JNI 路径(NMT),不在本篇展开
阶段产出
观测「是不是堆在漏」
hprof「谁占着内存」
MAT「为什么删不掉」
修代码remove / 限流 / 过期 / 注销 / try-with-resources

10. 速查卡

┌─────────────────────────────────────────────────────────┐ │ 信号: 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*:file=gc.log:time,level,tags

落地注意

  • jmap -dump:livejmap -histo:live都会触发 Full GC,勿在高峰频繁执行
  • 分析以Retained Heap为准;Shallow 大可能是被别的大对象引用。
  • 修复后做压测 + 长时间观察Old/Used 曲线,确认阶梯上升消失。
  • 堆外、JNI、本地库问题需NMT / 代码审查 / 原生工具,不能单靠 MAT。

本篇不适用

  • Metaspace OOMCodeCache栈溢出StackOverflowError)——需另查类加载、JIT、线程栈深度。
  • 纯堆外泄漏定位——需 NMT /pmap/ ASan 等,见 第 3 节 分流。

一句话:Java 堆泄漏排查 =Used 持续涨时抓hprof,用MATRetained 最大对象GC Root 引用链;先排除预热与缓存设计,再区分堆内(MAT)堆外/JNI(RSS + NMT)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/27 9:08:58

闪购bx-et算法分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 侵权通过头像私信或名字简介叫我删除博…

作者头像 李华
网站建设 2026/5/27 9:08:57

终极i茅台自动预约系统:基于Java Spring Boot的高效解决方案

终极i茅台自动预约系统&#xff1a;基于Java Spring Boot的高效解决方案 【免费下载链接】campus-imaotai i茅台app自动预约&#xff0c;每日自动预约&#xff0c;支持docker一键部署&#xff08;本项目不提供成品&#xff0c;使用的是已淘汰的算法&#xff09; 项目地址: ht…

作者头像 李华
网站建设 2026/5/27 9:08:22

res-downloader:3分钟搞定全平台资源下载的终极解决方案

res-downloader&#xff1a;3分钟搞定全平台资源下载的终极解决方案 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader 还在为无…

作者头像 李华
网站建设 2026/5/27 9:08:20

Gyroflow终极指南:如何利用陀螺仪数据实现专业级视频防抖

Gyroflow终极指南&#xff1a;如何利用陀螺仪数据实现专业级视频防抖 【免费下载链接】gyroflow Video stabilization using gyroscope data 项目地址: https://gitcode.com/GitHub_Trending/gy/gyroflow Gyroflow是一款革命性的开源视频稳定工具&#xff0c;它通过读取…

作者头像 李华