首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >OOM频繁发生别慌!手把手教你用工具揪出"内存刺客"

OOM频繁发生别慌!手把手教你用工具揪出"内存刺客"

作者头像
悠悠12138
发布2026-07-20 21:12:16
发布2026-07-20 21:12:16
150
举报

说出来你可能不信,我上个月又双叒叕被OOM折腾了一回。凌晨三点,应用告警群里疯狂弹消息,Java进程直接挂掉重启,看得我血压都上来了。

说实话,OOM这个东西吧,十个Java应用九个都遇到过。新手遇到它就是一脸懵,老手遇到它也得骂骂咧咧。这篇文章不整那些花里胡哨的理论,就实打实地聊聊当你的应用频繁OOM时,怎么用工具一步步揪出真凶。

先别急着dump堆,老规矩先看日志

很多人一遇到OOM第一反应就是"赶紧jmap dump堆啊",但其实这是错的。第一步永远是先看日志

Java应用在OOM的时候,默认会在控制台或者日志文件里抛出异常信息。这个异常信息很关键,它会告诉你到底是什么类型的OOM。常见的OOM有这么几种[1]:

  • java.lang.OutOfMemoryError: Java heap space:堆内存不够用了,最常见的一种
  • java.lang.OutOfMemoryError: GC overhead limit exceeded:GC回收了98%内存却还是腾不出空间,基本等于内存泄漏
  • java.lang.OutOfMemoryError: Metaspace:元空间满了,类加载太多
  • java.lang.OutOfMemoryError: Direct buffer memory:直接内存溢出,Netty玩多了容易遇到
  • java.lang.OutOfMemoryError: unable to create new native thread:线程数爆了,常见于线程池配置不当

我那次遇到的是 Java heap space,堆内存4G全被吃光了。但光看这个还不够,我得知道这内存是被谁吃掉的。

jstat:让你看清GC的全貌

光知道OOM的类型还不够,你得知道GC到底在干什么。jstat这个工具就是干这个的。

代码语言:javascript
复制
jstat -gc <pid> 1000

这个命令的意思是每隔1000毫秒输出一次GC信息。重点关注这几个列:

  • S0C/S1C: Survivor区容量
  • EC:Eden区容量
  • OC:老年代容量
  • YGC/YGCT:年轻代GC次数和总耗时
  • FGC/FGCT:Full GC次数和总耗时
  • GCT:GC总耗时

我当时看到的现象特别明显:FGC次数每小时涨几百次,每次耗时几百毫秒。这就说明老年代一直被填满,GC根本回收不动,典型的内存泄漏特征[2]。

还有个命令更直观,能看堆内存使用率:

代码语言:javascript
复制
jstat -gcutil <pid> 1000

输出里有个O列表示老年代使用率,如果这玩意儿长期在95%以上晃悠,那你基本可以确定是内存泄漏了。

jmap:把堆快照"拓印"下来

确认了是内存泄漏的问题,下一步就是dump堆。jmap是JDK自带的工具,用起来很简单:

代码语言:javascript
复制
jmap -dump:format=b,file=/tmp/heap.hprof <pid>

不过这里有个坑要说一下。dump堆的时候JVM会暂停所有线程,也就是STW(Stop-The-World)。如果你的应用是核心业务,最好在低峰期操作,不然可能引发雪崩。

还有一种更好的方式是配置JVM参数让OOM时自动dump:

代码语言:javascript
复制
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof

这样应用每次OOM都会自动生成一份堆快照,省得你半夜爬起来手动操作(我那次就是这么干的,设了自动dump后安心睡觉,第二天再分析)。

dump出来的文件一般会很大,我那次dump出来有6个多G。如果你要把文件拉到本地分析,建议先压缩一下

代码语言:javascript
复制
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>

加个live参数只保留存活对象,文件能小不少,传输也快。

MAT:内存分析的"屠龙刀"

拿到堆快照后,就该主角登场了——Eclipse MAT(Memory Analyzer Tool)。这玩意儿绝对是分析内存问题的神器,没有之一[2]。

下载安装就不多说了,官网下下来解压就能用。注意要改一下MemoryAnalyzer.ini里的内存配置,这个内存要大于你的dump文件大小,不然MAT会直接OOM(没错,分析工具自己OOM,听着是不是很魔幻)。

代码语言:javascript
复制
-vmargs
-Xmx8g

打开dump文件后,重点看这几个东西:

1. Leak Suspects报告

MAT会自动帮你分析可能的内存泄漏点,生成一份报告。点开就能看到哪些对象占的内存最多,哪些对象本该被回收却还在内存里"赖着不走"。

我那次MAT直接给我标红了一个HashMap,里面装着几十万条UserSession对象。这就是元凶了。

2. Dominator Tree(支配树)

这个视图能看到"谁支配着谁",也就是哪些大对象是其他对象的"主子"。在支配树里,最上面的一般就是占用内存最多的家伙。

3. Histogram(直方图)

按类的维度展示对象数量和占用内存。如果某个类的实例数明显异常,比如本应该只有几百个结果出现了几十万,那就是有问题的。

4. 引用链分析(Path to GC Roots)

这个最关键,能看到对象为什么没被GC回收。比如你发现有个UserSession对象一直没被回收,就可以用"Path to GC Roots"功能看它被谁引用着。结果一查,发现是被一个静态的HashMap长年累月地引用着,永远不会被释放

VisualVM:实时监控的小能手

如果说MAT是事后分析的神器,那VisualVM就是实时监控的利器[1]。它能让你看到内存的实时变化曲线。

VisualVM也是JDK自带的工具,JDK8之前直接在bin目录下有jvisualvm,JDK9之后需要单独下载。

用它主要干两件事:

  1. 实时监控内存曲线:看堆内存是不是有规律地增长,比如每次定时任务跑完就涨一波,那基本就是定时任务有问题
  2. 手动触发堆dump:在MBeans或者Monitor标签页可以直接生成堆快照

VisualVM还有个插件叫VisualGC,装上之后能看到各个内存分区的实时使用情况,包括Eden、Survivor、Old、Metaspace的实时数据。配合jstat用,绝配。

async-profiler:CPU和内存双修

有些内存问题不是单纯的堆内存泄漏,而是频繁创建大对象导致GC压力剧增。这种情况光看堆dump看不出来,得用profiler工具。

async-profiler是近两年比较火的一个工具,不像传统的JProfiler那么重,对生产环境的影响很小。可以用它来采样:

代码语言:javascript
复制
./profiler.sh -e alloc -d 30 -f /tmp/alloc.html <pid>

这个命令的意思是采样30秒内的内存分配情况,生成一个火焰图。从火焰图里能清晰地看到哪些代码路径在疯狂分配对象

我之前遇到过一个案例,应用每隔几分钟就Full GC一次,但用MAT看堆里又没多少对象。最后用async-profiler一查,发现是某个日志框架在疯狂创建StringBuilder对象,每次请求都创建几百个。这就不是内存泄漏了,是代码写法的问题。

元空间泄漏:容易被忽视的角落

说到OOM,还有一种特别容易被忽略的情况——元空间(Metaspace)溢出

JDK8之后,永久代(PermGen)被元空间取代了。元空间主要存放类的元数据,比如类信息、方法信息、字段信息等。如果应用加载了大量动态类(比如用了CGLIB、反射、或者JSP),元空间就可能被撑爆。

我之前有个项目用了大量的Groovy脚本动态编译,结果运行两天后元空间就满了。这种情况光看堆内存是看不出来的,得用:

代码语言:javascript
复制
jstat -gcmetacapacity <pid>

或者直接看JVM参数有没有限制元空间大小:

代码语言:javascript
复制
-XX:MaxMetaspaceSize=512m

没限制的话默认会一直涨,直到把机器内存吃光。

线程数爆了:另类的OOM

还有一种OOM是unable to create new native thread。这种情况不是堆内存的问题,而是线程数太多,把系统资源耗尽了

Linux下线程数是有限制的,可以通过ulimit -u查看。如果你的应用创建了大量线程(比如每个请求都new一个线程),很快就会达到上限。

排查这种问题比较简单,用jstack <pid>看看线程栈就行:

代码语言:javascript
复制
jstack <pid> > /tmp/thread.txt

然后统计一下线程数:

代码语言:javascript
复制
grep -c '^"' /tmp/thread.txt

如果线程数明显异常(比如上万),那基本可以确定是线程池没配好,或者代码里在某个地方死循环创建线程。

真实案例:那个被静态集合坑惨的夜晚

说回我那次凌晨三点被叫醒的故障吧。

应用是个电商系统,每到晚上高峰期就频繁OOM。dump堆之后用MAT分析,直方图里UserSession类有40多万个实例,每个大概1KB左右,占了将近400MB内存。

点开Path to GC Roots一看,路径是这样的:

代码语言:javascript
复制
UserSession 
  → HashMap$Node
    → HashMap 
      → XXXService (静态字段)

问题很明确,有个Service类里有个静态的HashMap,每次用户登录都往里塞一个Session,但从来不清除。改完之后直接用Caffeine缓存替代原来的HashMap,设置过期时间30分钟。线上效果立竿见影,内存使用率从95%降到了40%以下,OOM彻底消失。

总结:OOM排查的万能套路

最后总结一下我的排查套路,基本上任何OOM问题都能按这个流程来:

  1. 看日志:确认是哪种类型的OOM
  2. jstat看GC:判断是内存泄漏还是分配过快
  3. jmap dump堆:拿到第一手现场证据
  4. MAT分析:定位具体的泄漏对象和引用链
  5. async-profiler补刀:看代码层面的对象分配热点
  6. 改代码上线:解决问题后观察内存曲线

工具是死的,思路是活的。OOM这玩意儿看似可怕,但只要掌握了工具的使用方法,配合一点点耐心,基本上都能找到根因。

别忘了线上一定要配HeapDumpOnOutOfMemoryError这个参数,不然真的出事了,你连现场都没得查。

如果觉得文章对你有帮助,欢迎转发给你的同事朋友,运维路上一起升级打怪。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 运维躬行录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先别急着dump堆,老规矩先看日志
  • jstat:让你看清GC的全貌
  • jmap:把堆快照"拓印"下来
  • MAT:内存分析的"屠龙刀"
    • 1. Leak Suspects报告
    • 2. Dominator Tree(支配树)
    • 3. Histogram(直方图)
    • 4. 引用链分析(Path to GC Roots)
  • VisualVM:实时监控的小能手
  • async-profiler:CPU和内存双修
  • 元空间泄漏:容易被忽视的角落
  • 线程数爆了:另类的OOM
  • 真实案例:那个被静态集合坑惨的夜晚
  • 总结:OOM排查的万能套路
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档