定位Java内存溢出先抓堆转储和GC日志,再用MAT或jhat分析大对象引用链,同时别把内存泄漏和内存溢出混为一谈。 线上环境一旦抛出OutOfMemoryError,重启只能临时止血,保留现场、拿到heap dump和GC日志,再按下面路径一步步查,才能真正解决问题。
线上Java内存溢出怎么快速定位?先看三类信号
Java虚拟机内存溢出前,通常会发出三类信号:
- 异常类型信号:
java.lang.OutOfMemoryError: Java heap space指向堆内存不足,Metaspace指向元空间不足,unable to create new native thread指向线程数或本地内存耗尽。 - GC行为信号:
jstat -gcutil输出里,老年代占用持续攀升,Full GC执行频繁但回收后空间几乎不下降。 - 系统指标信号:CPU使用率异常升高,接口响应变慢,日志里出现大量超时,容器被OOM Killer强制杀掉。
先判断异常类型,再决定查堆、元空间还是线程栈,盲目调大-Xmx,对元空间溢出或本地内存溢出完全无效。
Java内存溢出和内存泄漏的区别,别把方向搞反
很多开发者把这两个词当成同义词,排查方向因此跑偏,内存溢出是一个结果,表示JVM申请不到所需内存,内存泄漏是一个原因,表示对象已经不再使用,却被强引用持续持有而无法回收。
| 对比项 | 内存泄漏 | 内存溢出 |
|---|---|---|
| 本质 | 对象引用未释放 | 内存需求超过可用值 |
| 表现 | 老年代使用率缓慢上升 | 直接抛出OOM异常 |
| 修复方向 | 查引用链,删除无意识强引用 | 调大内存或优化数据结构 |
| 常见场景 | 静态集合持续add、监听器未注销、ThreadLocal未清理 | 大文件读入byte数组、数据库查询无分页 |
行业共识认为,多数线上Java内存溢出背后都藏着内存泄漏,真正单纯因为堆配置太小导致的溢出反而占少数,所以先找泄漏,再考虑扩内存。
Linux服务器Java内存溢出调试命令清单
在Linux服务器上不一定有图形界面,但JDK自带的命令行工具足够完成大部分排查:
jps -l:列出Java进程PID。jmap -heap <pid>:查看堆配置和各区域使用情况。jmap -dump:live,format=b,file=/tmp/heap.bin <pid>:导出存活对象的堆转储文件。jstat -gcutil <pid> 1000 10:每秒输出一次GC回收统计,连续10次。jstack <pid> > /tmp/thread.txt:导出线程栈,排查线程创建异常。jcmd <pid> GC.heap_dump /tmp/heap.bin:新版本JDK推荐的堆转储命令。
如果生产环境不允许停机,使用jmap -dump:live可能触发一次Full GC,建议在低峰窗口执行,更稳妥的是在启动参数里提前加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.bin,让JVM在抛出OOM时自动保留现场。
拿到堆转储文件后,四步定位大对象
堆转储文件是内存溢出排查的核心证据,分析过程不用靠猜,按四步走:
- 看概要报告:用Eclipse MAT打开
heap.bin,先看Leak Suspects Report,它会列出可疑大对象和引用路径。 - 看支配树:切换到
Dominator Tree视图,按保留内存大小排序,排在最前面的几个类往往就是元凶。 - 查实例数量:用
Histogram按类名统计实例数和占用内存,如果某个业务对象实例数异常多,比如出现百万级OrderVO,基本可以锁定范围。 - 追GC Root引用链:右键可疑对象,选择
Path To GC Roots,排除弱引用和软引用,看强引用路径,路径终点常常是一个静态HashMap、ThreadLocal变量或长期存活的对象缓存。
MAT里三个最常用的排查视角
- Leak Suspects Report:自动分析并给出嫌疑报告,适合新手快速上手。
- Dominator Tree:按保留内存大小排序,直观看谁占着最多的堆。
- Histogram:按类名统计对象数量和内存占用,适合对比不同时期的dump文件。
用jstat和GC日志判断是突发溢出还是缓慢泄漏
并非所有内存溢出都是泄漏,用jstat -gcutil <pid> 1000持续观察:
- 如果老年代使用率每隔几分钟增长一两个百分点,Full GC后也降不下来,说明存在缓慢泄漏。
- 如果某次大请求进来后老年代瞬间打满,平时却能正常回落,那是突发大对象加载,比如一次性读取整张数据库表。
GC日志里重点看Full GC前后老年代回收率,回收率越低,泄漏越明显,再配合jmap -histo:live <pid>观察存活对象数量是否持续增加,能进一步确认。
真实线上场景:一次接口调用导致的内存溢出定位
假设某个电商系统的订单导出接口上线后频繁触发OOM,排查过程如下:
- 步骤1:
jps -l找到服务PID,例如12345。 - 步骤2:
jmap -dump:live,format=b,file=/tmp/order-oom.bin 12345。 - 步骤3:本地用MAT打开,
Histogram里发现OrderExportVO实例数量达到千万级,保留内存高达几个GB。 - 步骤4:
Path To GC Roots显示这些对象被一个ArrayList持有,而ArrayList是导出方法里的局部变量。 - 步骤5:查看代码,导出方法里
list.add(orderVO)没有分页,查询条件又不带索引,导致数据库返回全部订单,方法执行期间内存持续膨胀,直到OOM。
修复方式是给查询加分页,并改为流式写入,不再把全量数据放在List里,这个案例说明,内存溢出定位的关键是让堆转储说话,而不是改参数碰运气。
Java内存溢出解决方案的成本对比
定位到问题后,解决方案有几种,成本差异也很大:
- 调大堆参数:改
-Xmx和-Xms,零资金成本,但只能缓解,治标不治本。 - 排查并修复代码泄漏:需要开发人员投入时间,适合多数情况,长期收益最高。
- 使用商业诊断工具:JProfiler、YourKit等商业版按年授权,价格通常在每年数百到数千元,MAT、Arthas、VisualVM免费居多。
- 第三方性能调优服务:北京地区Java内存溢出诊断服务按次收费,多数在数千元级别,适合紧急线上事故但团队能力不足的场景。
多数情况下,先用免费工具自查就能解决,只有遇到特别复杂的类加载器泄漏或长时间无头绪时,再考虑付费服务。
定位Java内存溢出不是靠重启和猜,抓住异常类型、堆转储、GC日志三条线,用MAT找到大对象引用链,再对比内存泄漏和内存溢出的区别,大部分问题可以在半天内定位到具体代码,平时在代码评审中注意静态集合、ThreadLocal和监听器的清理,比任何调优技巧都管用。
Q&A
Java内存溢出怎么排查最快?
最快路径是:先看OutOfMemoryError后面的类型,然后用jmap -dump导出堆转储,再用MAT的Leak Suspects Report自动分析,这一套流程在多起线上事故中都能在十几分钟内锁定嫌疑类。
Java内存溢出和内存泄漏是一回事吗?
不是一回事,内存泄漏是对象不再使用但被强引用持有,内存溢出是内存申请失败,泄漏只是溢出的原因之一,还可能因为堆内存参数过小、大对象加载、元空间不足或本地内存耗尽。
生产环境没有图形界面怎么定位Java内存溢出?
在Linux上使用jmap -dump导出堆转储文件,再把文件拉到本地Windows或Mac上,用MAT打开分析,同时在服务器上用jstat -gcutil观察GC趋势,用jstack排查线程和死锁,JDK命令行工具完全支持headless环境下的证据采集。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/647438.html





