ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JVM OOM排查实战:从Arthas到MAT的完整工具链指南

JVM OOM排查实战:从Arthas到MAT的完整工具链指南 先说说我踩过的坑。第一次独立处理线上OOM是在一个周四傍晚订单服务突然疯狂Full GC接着直接OutOfMemoryError。我当时第一反应是登录服务器结果发现现场特别尴尬不敢重启、dump太大没法下载、日志里全是java.lang.OutOfMemoryError: Java heap space但看不出是谁干的。当时我的排查命令一个接一个地试jmap、jstat换着来因为经验不足甚至误触发了生产环境长时间停顿。后来我慢慢把这套工具链摸熟了Arthas、MAT、JProfiler、JDK自带工具、Async-Profiler都用过也踩过不少坑今天就把这些工具的使用心得、优劣对比和选型思路一次性讲清楚。很多团队对OOM的认知还停留在加内存或者重启大法层面但其实OOM只是表象背后可能是代码泄漏、大对象分配、集合无限增长、甚至直接内存泄漏。要定位真实根因手上必须有一套趁手工具。这篇文章适合后端开发、运维、SRE以及所有跟JVM应用打交道的人从常用命令到高级性能剖析工具我会按实际使用场景来拆解。1. 破局第一步理解OOM的类型与排查思路在动手用工具之前我建议你先搞清楚一个核心问题你遇到的OOM是哪种类型不同OOM类型排查路径完全不同用工具的重点也完全不同。1.1 四种常见OOM类型与定位方向JVM抛出OutOfMemoryError时通常会附带一段明确提示这一段提示直接决定了你的排查方向。Java heap space堆内存耗尽是最常见的OOM类型。通常是对象过多、集合未释放、大对象分配过多、或者存在内存泄漏。排查重点放在堆转储分析、GC日志和对象分配统计上对应工具就是MAT、jmap、Arthas的heapdump和JProfiler的Heap Walker。GC Overhead Limit Exceeded98%的时间在做GC但回收不到2%的内存。这其实是堆内存被占满的另一种表现底层原因依然是内存耗尽或严重泄漏只是JVM先通过这个错误告诉你有问题。排查思路和heap space相同但还要额外检查是不是GC参数配置不合理。Metaspace元空间耗尽类元数据太多。典型场景是动态生成类、热部署、反射调用未清理、或者CGLib大量创建代理类。排查重点放在类加载统计上用jstat的Metaspace指标、JProfiler的Classes界面或者Arthas的class/classloader相关命令。Direct buffer memory直接内存耗尽很多NIO框架Netty特别典型会中招。堆内存明明很健康但应用崩溃了。排查方向是统计DirectBuffer使用量用jcmd的话可以用VM.native_memory或者JProfiler的内存剖析Async-Profiler也能做native内存采样。我建议所有团队成员都先养成一个习惯看到任何OOM报错第一件事不是找工具而是先截图完整的报错堆栈和OOM异常的message。这个信息看似简单但能帮你把排查范围至少缩小一半。1.2 通用排查四步法不管用哪套工具我个人建议整个排查流程固定成下面四步避免在现场乱了阵脚先看系统层用top确认哪个进程内存异常再用free看物理内存是否真的耗尽排除容器限制导致的“假OOM”。再看JVM层用jstat看GC频率和heap占用趋势用jstack看线程状态确认是内存问题还是并发问题引发的连锁反应。拿到堆转储用jmap或Arthas导出heapdump用MAT/JProfiler/Arthas进行对象级分析。定位代码根因结合dump中的支配树、GC根路径、或者火焰图找到具体的分配点和大对象来源修复代码并验证。这套流程看着简单但真正难的是在OOM现场“保留现场”。因为很多线上事故一发生就被自动重启脚本拉起来了dump文件也没留下。后面我会讲到Arthas可以在不重启的情况下事后排查这是它的巨大优势。2. 先拿现成的JDK自带工具是OOM排查的第一梯队JDK自带工具最大的优点就是零成本、随装随用任何一台装了JDK的机器上都有。你不需要下载任何第三方软件也不用担心生产环境不让装agent。很多新手往往会忽略它们但我个人认为这是OOM排查的“地基”。2.1 从jps和jstat看进程与GC状态排查的第一幕永远是确认哪个Java进程出了问题。jps -l可以列出所有Java进程的PID和主类像这样jps -l # 输出示例 # 14256 /data/app/order-service.jar # 23114 sun.tools.jps.Jps拿到PID之后jstat是我们诊断GC趋势的利器。我最常用的命令是jstat -gcutil pid 1000 10这个命令的意思是每隔1000毫秒输出一次GC统计共输出10次。-gcutil输出的列是各内存区域的使用百分比。我特别关注YGC、FGC和FGCT。如果FGC频繁且FGCT很大说明老年代已经被撑满且Full GC已经成了常态。这时候即使没有正式OOM系统也已经非常危险。可以配合jstat -gccause查看最近一次GC的原因快速判断是不是因为内存分配失败触发。如果你拿到的是一台已OOM但进程还没退出的机器jstat能帮你确认崩溃前的状态。但如果进程已经退出jstat就无能为力了你只能依赖日志和dump文件。2.2 jmap与jcmd获取堆信息的两条路jmap是老牌堆分析工具但我要先提醒你一个重点线上的慎用jmap -dump。因为导出堆转储是Full GC式操作会引起STW停顿。我见过有团队在生产高峰期执行jmap -dump结果服务卡了十几秒业务雪上加霜。如果你非要用建议先加上-dump:livetrue只导出存活对象但也别指望它停顿很轻。实际经验是一个4G堆的进程轻量dump也要几秒停顿。如果是查看堆概况jmap -heap比较安全但1.8部分版本中它底层也会有STW风险输出内容大致包括堆内存参数配置、各代的空间使用情况。jmap -histo:live则能列出类实例数量和占用空间对快速发现异常大对象很有效比如你看到byte[]占了总堆的70%那基本可以锁定缓冲区相关代码。jcmd是JDK 8之后更推荐的底层排查命令定位更高端。它的用法是jcmd pid GC.heap_dump /data/dump/heap.hprof jcmd pid VM.native_memory summaryjcmd的GC.heap_dump和jmap -dump效果类似但它有更细的JVMTI控制能力。VM.native_memory要配合启动参数-XX:NativeMemoryTrackingsummary或detail来使用可以分析直接内存和native内存这在排查Direct buffer memory时是核心手段。不过每次开启NMT都有约5%的性能损耗建议只在定位问题时短时间开启。2.3 jstack与线程转储排查线程引起的间接OOM不是所有OOM都来自堆对象分配。有一种隐蔽场景线程创建太多导致无法分配线程栈抛出OutOfMemoryError: unable to create native thread。此时jmap可能显示堆很健康真正的元凶是操作系统线程数上限。用jstack -l 抓取线程堆栈后统计线程数量、观察线程状态分布很有帮助。我遇到过一次典型的unable to create native thread问题jstack里看到几千个WAITING状态的线程全是HTTP连接池不释放导致的。线程本身不占heap但每个线程栈需要native内存积少成多就把进程内存耗尽了。命令行工具胜在零成本和直接我个人的经验是生产环境优先用jcmd和jstat进行初步观察jmap -dump尽量放在低峰期执行并且最好配合后面要讲的Arthas先做预先标记再决策是否dump。3. 能救命的神器Arthas在生产环境的热诊断能力如果你在OOM现场只能带一件工具我强烈推荐Arthas。它最大的价值在于“不需要重启进程”而且可以动态attach到正在运行的Java进程上直接查看运行时的类、对象、调用栈、甚至GC情况解决了线上问题“重启就丢现场”的难题。3.1 Arthas快速上手与三个OOM核心命令Arthas的安装和启动非常简单curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar启动后它会列出当前机器上的Java进程输入序号回车即可attach。这个attach过程本身是安全的不会导致应用重启但建议仍然不要在业务高峰期操作因为attach本身有一定开销。启动后进去的是一个交互式命令行界面直接用命令操作。针对OOM场景我的“三个命令组合”是dashboard一屏看全部关键指标。它能显示进程的CPU、内存、GC情况、线程信息。当你想快速了解当前服务器上这个JVM的整体状态时dashboard是最直观的。尤其是在现场不知道从哪查起的时候dashboard能帮你建立全局认识后面再决定针对哪个对象或线程深入分析。thread查看线程快照配合参数可以找出“罪魁祸首”线程。比如我们常怀疑有某个线程在无限创建对象可以用thread -n 3查看CPU占用最高的三个线程并且可以看到每个线程的堆栈直接定位到业务代码。命令thread -b还能找出当前阻塞其他线程的锁对排查线程死锁引发的内存问题很有用。heapdumpArthas内置了堆转储下载接口用法如下heapdump --live /data/dump/arthas-heap.hprof这个命令可以在进程存活状态下导出堆dump而且比裸跑jmap安全一些因为它走了更可控的路径。导出完之后文件路径会显示在Arthas控制台然后用sz或者scp拉到本地交给MAT进行深度分析。还有个命令是logger它可以查看当前应用的日志配置。OOM发生前如果日志疯狂刷屏logger命令能帮你快速找到是哪个logger配置导致日志量失控日志刷屏也是OOM的一个常见推手。3.2 用Arthas做火焰图的实战技巧你可能听过大名Arthas的profiler命令底层集成了async-profiler能够生成火焰图。这条命令对OOM排查的价值在于当你怀疑是CPU长时间满负荷导致的GC频繁、进而引发内存问题火焰图可以一眼看出CPU时间都消耗在哪个方法栈上。用法很简单profiler start # 压测或等待业务运行一段时间 profiler stop --format html -o /data/profiler/arthas-flame.html生成的文件是HTML格式用浏览器打开就能看到火焰图。火焰图横轴是采样到的调用栈次数宽度越宽说明消耗越多CPU纵轴是调用层级。从上往下看你可以顺着最宽的“平顶”找到热点方法。我之前排过一个OOM本质上是一个正则表达式在大文本上执行了灾难性回溯CPU被打满GC也跟着疯狂工作。用Arthas的profiler火焰图一照那个正则方法的长条非常明显。有一点要补充profiler在JVM里做的是采样而非精确计数因此对性能影响很小可以短时间在生产使用。但火焰图解决的是性能剖析问题不能直接证明某处泄漏内存它是内存分析的辅助证据。3.3 Arthas的动态类观察对排查泄漏代码的作用很多时候我们虽然知道是某个集合无限增长但不知道增长发生在哪个方法。Arthas的watch和trace命令可以动态观察方法的入参、返回值和异常不需要改代码、发版本就能在线上盯着某个可疑方法。比如代码里有一个addToList方法怀疑它把对象无脑加入静态List就可以watch com.example.OrderService addToList {params[0], returnObj} -x 2只要这个方法的调用一发生控制台就会打印出参数和返回值连续观察几秒你是不是能看到某个对象被源源不断塞入集合写的频率和大小如果非常大那基本可以敲定就是这里。这种方法在代码仓库定位困难、逻辑复杂时特别高效实测已经帮我在多个项目中把OOM根因精确到了具体函数。4. 离线深挖现场MAT的堆转储分析全解析拿到heapdump文件之后真正累人的分析才刚开始。Eclipse MATMemory Analyzer Tool是业界公认的堆分析标杆。它的口号就是“1G堆内存的dump也能在合理时间内分析完”并且是免费的。我个人用MAT分析过的dump文件累计已经有几十个从几百MB到几个GB都有稳定的体验让我一直把它当主力。4.1 MAT的安装、导入与初始页面MAT可以独立安装也可以作为Eclipse插件建议直接下载独立版下载地址是eclipse官网的Memory Analyzer Releases页面。注意选择与你OS匹配的版本比如Windows 64位选mat-1.13.0-win32.win32.x86_64.zipmacOS选macosx.cocoa.x86_64.dmg。打开后通过File - Open Heap Dump导入你的.hprof文件。第一次打开较大的dump可能会弹出一个提示询问是否接受Leak Suspects Report之类的分析一般选Yes即可。MAT会生成一份报告最醒目的是首页中央的Overview和“Leak Suspects”。Leak Suspects视图会列出“Problem Suspect 1”、“Problem Suspect 2”等内容每个嫌疑项都有一段描述比如“The class java.util.ArrayList, loaded by... occupies X bytes”并且可以直接点击进入详情。这份报告能帮你快速判断主要内存消耗点但不一定直接指向代码还需要进一步研究。4.2 Histogram直方图与List Objects的使用Histogram是按类统计所有实例数量和占用空间的列表。进入Histogram后你会看到类似这样的列Class Name、Objects、Shallow Heap、Retained Heap。Shallow Heap对象自身占用不包括引用子对象。Retained Heap对象本身及其被它引用对象所占的内存总和是判断“谁真的大”的核心指标。我通常的做法是先把Histogram按Retained Heap降序排列看前20个类。如果byte[]和char[]排在前面说明字符串和字节缓冲区是大头如果某个自定义业务对象霸榜那就去查看它的引用关系。点击某个类后选择List objects - with incoming references能列出该类所有的实例。此时选中一个实例右键-Path to GC Roots - exclude weak references可以看到这个对象是怎么被JVM根引用链保持的。这条GC根链就是证明它“不能回收”的关键论文据顺着链条能找到StrongReference的位置进而找到持有这个对象不释放的业务代码。4.3 Dominator Tree支配树快速锁定罪魁祸首Dominator Tree是MAT最实用的视图之一没有它我们几乎没法在几百万对象中找泄漏点。支配树展示的是对象的“支配者与被支配者”关系你可以显式看到哪些对象占用了最多的保留空间。一棵树根节点往往就是问题集合或者问题对象。我自己遇到过一个经典案例支配树顶部是一个ConcurrentHashMapretained size占了堆的60%。顺着往下看value是几十万个Session对象最后发现代码里把每次请求创建的Session放进了静态缓存却没有任何清理逻辑。DOM Tree直接把根因钉在眼前。用Dominator Tree的注意事项是它计算的retained size有误差对多线程下的共享对象尤其如此但用作排查线索完全够。你不需要看每一个节点只看最粗的那几条分支。4.4 MAT分析时的常见陷阱用MAT这么多年我总结了一些避坑经验。第一分析超大dump时建议调整MAT的内存参数。默认配置可能只给512MB解析3GB以上的dump直接OOM。修改MAT安装目录下的MemoryAnalyzer.ini文件将-Xmx1024m这类参数改为-Xmx4096m甚至更高然后再启动。我自己分析8G dump时会把-Xmx设成10g毕竟解析时还要保留大量对象引用关系。第二dump文件本身尽量用压缩格式传输hprof文件可以用zip压缩后再传在MAT里打开压缩包是没问题的省流量也省时间。第三MAT的分析结果只能反映“那一刻”的内存状态。如果你dump的时间点已经偏离泄漏高峰太久可能看不出明显问题这时建议对比两个时间点的dump用MAT的Compare Tables功能对比Histogram的差异看哪些类在增长、增长多少。这个对比方法在排查慢速泄漏时几乎是必须的。5. 重型武器与全能选手JProfiler与Async-Profiler的剖析价值5.1 JProfiler从安装到连接生产环境的完整实战JProfiler是一款商业级Java性能剖析工具功能非常全面图形化界面比命令行工具友好太多。它支持CPU剖析、内存剖析、线程剖析、JDBC/JMS探测等。如果公司有预算团队可视化排障体验会提升一个档次。使用JProfiler最经典的生产环境方式是在需要诊断的Java进程启动参数中加入JProfiler agent参数然后通过IDEA或独立客户端远程连接。例如-javaagent:/path/to/jprofiler/bin/agent/jprofilerti.soport8849,nowait服务启动时会加载这个agent监听8849端口。然后你本地的JProfiler客户端选择Attach to remote JVM填入服务器IP和端口即可。这里有个关键服务器防火墙和云安全组必须放行该端口。生产环境如果不想开放端口也可以改用离线会话模式将分析数据写入本地文件事后导入JProfiler分析。JProfiler的Heap Walker堆遍历器是MAT的强有力补充。它的UI能实时浏览对象实例不用手动找GC根路径。更强大的是Allocation Recording功能可以重新记录某个时间段的分配栈。比如你想知道哪些代码路径创建了大量byte[]打开Allocation recording期间访问相应功能JProfiler就能告诉你每个分配点的栈路径。这在定位“哪里分配了内存”上比MAT更直观。JProfiler还可以方便地查看类加载情况、数据库调用时长。如果OOM的元凶是你项目里某个ResultSet没关导致连接池暴涨JProfiler的JDBC监视能帮你快速定位到具体SQL和调用栈。不过要注意开启JProfiler的CPU和内存分析会带来一定性能开销生产环境谨慎使用最好事先评估CPU余量。5.2 Async-Profiler低开销火焰图与native内存剖析Async-Profiler是一款基于AsyncGetCallTrace的低开销采样剖析器也是Arthas底层使用的火焰图引擎。它的核心思路是每隔一个很小的间隔采样各线程的调用栈从而统计热点方法、内存分配热点以及native方法使用。用法也简单下载对应平台的async-profiler用以下命令完成采样./profiler.sh -d 60 -o flamegraph -i 5000 pid /data/flame.html-d 60表示采样60秒-i 5000表示每5000纳秒采样一次-o flamegraph指定输出格式。生成的文件用浏览器打开火焰图效果比Arthas内置版更容易看懂。Async-Profiler的alloc采样模式对排查内存分配热点尤其有用./profiler.sh -d 60 -e alloc pid -o collapsed输出结果可以看到不同调用栈分配内存的比例不用dump也能判断哪些路径在疯狂分配对象。我曾用它定位过一次Eureka客户端心跳线程在短时间内不断创建更新状态对象导致堆内存持续上涨的问题非常精准。需要留意的是Async-Profiler对Linux内核版本有要求某些云厂商的旧内核可能不支持需要带参数运行或者寻找特定版本编译产物。它在生产环境的开销极低大约在2%到5%之间完全可以短时间使用。5.3 两种剖析工具的差距与定位差异JProfiler和Async-Profiler虽然都能做性能剖析但侧重点和使用场景有较大差异千万不要混为一谈。JProfiler更像一个全维度的可视化监控平台适合定期做压测、稳定性测试和细微问题排查。它支持的功能包括CPU、内存、锁、磁盘、JMS、JDBC等适合开发阶段和预发环境做深度的代码质量剖析。因为它是图形界面所以学习成本不高团队协作时特别容易上手但商业授权费用不低。Async-Profiler则是一个命令行轻量级性能剖析工具体积小、启动快、开销低聚焦在火焰图和分配热点。它没有繁琐的图形界面适合在已经定位到某些方向后做针对性的快照不适合“边操作边观察”的探索式排障。优点是开源免费和Arthas深度绑定服务器上通常已经存在。如果只用一句话概括JProfiler是“体检中心”Async-Profiler是“机场安检”。6. 工具横向对瞻一张表看清OOM排查工具怎么选很多同事问过我到底该用Arthas还是MAT该装JProfiler还是用JDK自带工具。我的答案是没有哪款工具是全能的正确的做法是按阶段分层使用。这里的对比表可以帮你快速决策工具核心能力适用场景性能开销费用上手难度JDK自带jps/jstat/jmap/jcmd/jstack进程、GC、堆栈快照现场第一反应快速了解状态jmap -dump有STW风险其余较低免费低但需熟练记忆命令Arthas在线动态诊断、heapdump、火焰图、方法追踪生产环境不重启、定向追踪、事后还原attach与命令有少量开销总体可控免费开源中等命令交互需练习MAT离线堆转储深度分析dump精准定位泄漏对象与GC根路径不影响运行进程仅分析时吃本地内存免费开源中等重点是理解支配树JProfiler全维度剖析CPU、内存、线程、数据库、JDBC开发/预发深度调优、一体化可视化排障剖析开销较大生产需谨慎商业付费中低图形界面友好Async-Profiler低开销采样火焰图、分配热点线上低开销CPU与内存分配剖析极低采样开销约2%-5%免费开源低单条命令如果以一次线上OOM的完整生命周期为例子我的推荐组合是刚发现OOM时先上JDK自带命令快速确认进程状态和各内存区域占用。如果需要保留现场用jcmd或Arthas导出heapdump同时用jstack抓取线程快照。进程不能重启的场景优先上Arthas的dashboard、thread、watch疑点确定后继续。拿到dump后用MAT做离线对象级分析找到泄漏点与GC根。如果是性能问题导致的间接OOMCPU打满引起GC膨胀上Async-Profiler生成火焰图。如果是项目开发阶段或者公司预算充足可以常态化用JProfiler做日常代码审查和性能回归。7. OOM排查的实战经验与避坑清单光会工具是不够的实战中的“分寸感”才是让我成长最快的地方。我在这里专门总结一些多数文档不会写但实际工作里几乎天天踩的坑。遇到OOM先看是不是“假OOM”。容器内存限制、Swap混乱、物理内存不足都可能导致JVM外进程被kill而非JVM自身OOM。有的云环境是cgroup内存限制触发杀进程应用日志里根本没有OutOfMemoryError。这种时候用dmesg看看有没有oom-killer的记录别一头扎进jmap。dump前先确认进程是否还安全。我见过有人在高流量时段执行jmap -dump直接把服务搞挂。如果你的进程已经极端不健康选择Arthas的heapdump或者jcmd并且尽量限流、在业务低谷执行。稳妥的方式是先挂上jmx或者用GC日志评估停顿风险。线上别乱调-XX:HeapDumpOnOutOfMemoryError。这个参数在OOM时自动导出dump非常推荐开启但是要注意它生成的dump路径是否在数据盘上否则可能把系统盘写满反而加重故障。配置了该参数后记得确认文件命名和路径比如-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump保留现场优先级极高。进程一旦被重启所有内存状态都没了。建议在JVM启动参数里就开启自动dump并且配置监控脚本在OOM前后自动抓jstack、top、网络连接等上下文信息。这类“建档保存”能极大减少事后猜谜。dump文件分析要“有目标”不要漫无目的地看每个对象。内存上百万个对象时没有目标的浏览等于大海捞针。先借助MAT报告、支配树或者Histogram锁定排名前几的类再往下细化才能高效。注意线程栈大小。OOM时除了堆dump线程dump往往能告诉你问题是不是在线程爆发上。很多团队只重视堆而忽略线程结果排查方向完全跑偏。如果你是SRE或运维建议考虑把Arthas纳入日常运维基座它是所有工具中与线上环境最贴合的一个。8. 选型建议与一份可直接落地的命令清单最后给出一份我调试多年沉淀下来的“工具箱”使用路线图你可以直接保存收藏。8.1 按角色选择工具如果你是开发人员处理自己负责的应用的OOM优先用Arthas做在线追踪、MAT做离线分析。如果平时就喜欢可视化申请预算用JProfiler。如果你是运维或SRE不仅是恢复应用还要迅速判断影响面和根因首选JDK自带命令Arthas的快速诊断配合Async-Profiler做低开销剖析。不要一上来就用JProfiler它的开销和配置会让运维事故现场更复杂。如果你是团队负责人或架构师建议把整套工具链标准化进运维文档并统一配置JVM启动参数例如开启自动dump、GC日志、NMT开关。同时设计一套标准的排查SOP让新人也有章可循。8.2 一张实用命令速查表目标推荐命令/工具说明快速查看进程列表jps -l获取PID查看GC和内存趋势jstat -gcutil 1000 10观察多次输出查看类实例直方图jmap -histo看占用TOP类导出完整堆转储jcmd GC.heap_dump /path/dump.hprof备选Arthas heapdump在线看线程状态jstack关注线程数、状态在线查大对象/调用链Arthas dashboard/watch/trace精确到业务方法导出火焰图Async-Profiler 或 Arthas profiler start/stop定位CPU热点和分配热点离线堆分析MAT Histogram Dominator Tree锁定GC根路径全面可视化剖析JProfiler Heap Walker Allocation Recording适合预发深度分析我个人在实际操作中的体会是工具越多越要克制。每次OOM排查先固定“阶段”再选工具而不是把所有工具全上。尤其在生产环境先保证服务不挂再保证取证充分最后才做深挖。上面这些命令和流程是我在无数次熬夜排障之后沉淀下来的希望能让你在遇到那个红色报错时少走几步弯路。下次系统再报OutOfMemoryError记得深吸一口气按这套思路来。
返回列表