堆内存溢出(Heap OOM)排查方法
Heap OOM,说白了就是JVM的堆内存被撑爆了。我的经验是,项目上线后80%的OOM都跟堆有关系。你想想看,对象不断创建,回收不过来,最后堆满了,系统就崩了。
怎么排查?我习惯用三个工具:jmap、jstat 和 VisualVM。它们各有分工,组合起来基本能覆盖所有场景。
jmap:堆内存的“快照机”
jmap 能直接告诉你堆里现在是什么情况。我最常用的几个命令:
# 查看堆配置和当前占用
jmap -heap <pid>
# 生成堆转储文件
jmap -dump:live,format=b,file=/tmp/dump.hprof <pid>
-heap 会输出新生代、老年代的容量和使用量,以及GC算法的配置。我个人习惯先用这个命令快速扫一眼,看看哪个区压力大。
有一次我在线上看到一个服务,老年代已经占到了92%,新生代却空得很。这明显是对象过早晋升了,直接调大老年代没用的,得先查为什么这些对象活那么久。嗯,这里要注意,别盲目改参数。
-dump:live 会触发Full GC,生产环境慎用。如果一定要用,我建议在业务低峰期执行,或者先做一次流量摘除。
jstat:GC行为监控利器
jmap 只看静态快照,但内存问题是动态的。这时候就要用 jstat 了。我最常用的两个场景:
1. 监控GC频率和耗时
jstat -gc <pid> 1000 10
这条命令每秒输出一次GC信息,连续10次。输出结果里我最关注三个指标:YGC(Young GC次数)、YGCT(Young GC总耗时)、FGC(Full GC次数)。
我记得有一次排查,YGC每秒钟触发10多次,每次耗时却只有几毫秒。乍一看好像还好,但仔细算一下,每秒有10%的时间在GC上。这种频繁的Minor GC,其实已经把应用拖慢了。
2. 看GC后内存变化趋势
jstat -gccapacity <pid> 2000 5
这个命令能告诉你各区域的内存容量和可用空间。我一般连续采集5-10分钟,观察老年代是否在持续增长。如果每次GC后老年代只增不减,那就是内存泄漏的典型信号。
我个人的小技巧:把 jstat 输出重定向到文件,用Excel或Python做个趋势图,一眼就能看出内存是否在“螺旋上升”。
VisualVM:图形化监控
如果服务器有GUI环境,或者你愿意用远程JMX,VisualVM 是最直观的工具。它集成了 jmap 和 jstat 的功能,还多了CPU和线程的监控。
我个人习惯这样用:
- 监视标签:看堆内存使用量的实时曲线。如果曲线像“锯齿”——涨上去降下来,那是正常的GC;如果一直往上不回头,就是泄漏。
- 堆Dump:点一下就能生成dump文件,而且能直接在工具里看对象分布。我最喜欢用的大对象分析功能,能列出占用内存最多的对象,按类名排序。
- CPU采样:有时候内存问题其实是代码问题,比如某个方法创建了大量临时对象。用CPU采样找出热点方法,往往能顺藤摸瓜找到内存元凶。
一张图看懂排查流程
上面说的三个工具,怎么配合使用?我画张图帮你理清思路。
实战中的几点经验
| 场景 | 推荐工具 | 我个人的判断技巧 |
|---|---|---|
| 突然OOM,想快速定位 | jmap -dump | 先别急着重启,生成dump文件再重启,否则线索全丢 |
| 服务变慢,怀疑GC问题 | jstat -gc | 连续监控5分钟,看YGC是否翻倍增长 |
| 做长期内存健康检查 | VisualVM + 定时dump | 在非高峰时段自动抓取dump,对比分析 |
| 排查大对象泄漏 | VisualVM 的堆Dump分析 | 按“保留大小”排序,顶部对象就是元凶 |
曾经我遇到过这样的教训:有次线上OOM,我直接重启了服务,然后用jmap去连。结果连不上了——因为进程已经没了。那之后我养成了一个习惯:在服务的启动脚本里加一个OOM后的自动dump钩子,用 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps。这个参数能救命。
其实排查Heap OOM并不神秘。你按照jmap看快照、jstat看趋势、VisualVM做可视化这条路走,十有八九能定位到问题。嗯,今天就先讲到这里,下回再聊聊具体怎么分析dump文件里的那些对象。