ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MAT实战:从dump文件到定位OOM真凶的完整排查指南

MAT实战:从dump文件到定位OOM真凶的完整排查指南 做JVM调优或者线上排查的人几乎都会遇到这么一天主机的CPU突然飙到100%或者接口的响应时间从小几十毫秒直接跳到几秒再严重一点OOM直接把进程干崩了。服务重启之后好像又一切正常但你心里清楚根因没找到下次还得爆炸。我也是在经历过几次深夜被连环报警叫醒之后才认认真真把MATMemory Analyzer Tool这套路子完全吃透。这文章不聊那些面试题里背得滚瓜烂熟的JVM内存模型理论而是实打实地讲清楚当你手里拿到一份dump文件时到底该怎么一步步拆解怎么从上百万个对象里把那个吃掉内存的“真凶”揪出来。1. dump文件分析前必须搞清的几个核心问题很多人一上来就对着MAT界面发懵满屏的百分比和字节数根本无从下手。其实不是MAT难用而是你还没想清楚自己到底要问它什么。在双击打开MAT之前先问自己五个问题答案越清晰后面的分析就越快。1.1 dump文件到底是什么它记录了哪些信息可以把dump文件理解成JVM在某个时刻拍下的一张“内存全景照片”。它不只是一个简单的数字快照而是包含了完整的JVM堆内存状态信息包括所有Java对象、对象之间的引用关系、每个对象的类信息、类加载器信息、GC Roots的引用路径甚至还包括线程栈和局部变量信息。和技术面试里常背的那些概念挂钩的话它就是JVM内存模型里“堆区”的实体化呈现。hprof二进制格式的文件里面每个对象都有地址、类型、大小、引用和被引用关系。这就意味着拿到dump文件之后你不仅能看出来当前堆里有多少对象还能顺着对象之间的引用链往上走一步步追溯“到底是谁还握着这个对象不让垃圾回收器回收它”。这就是MAT最核心的价值所在——让你在茫茫的对象海洋里找到那条导致内存问题的引用链。1.2 不是所有内存问题都需要分析dump文件这里必须先泼个冷水。有些内存问题完全不需要dump分析就能解决甚至分析dump反而绕远路。比如纯代码里写了个死循环往List里塞对象这种Case你用dump当然也能抓出来但实际上看一眼GC日志里有没有高频的Full GC就八九不离十了。再比如堆参数设置不合理Eden区太小导致Young GC频繁这种问题也未必需要dump。真正必须借助dump文件深度分析的是内存泄漏类问题、大对象持续增长问题、类加载器卸载失败这类“看不见摸不着”的疑难杂症。一个基本的判断标准是进程从启动到稳定运行堆内存在Full GC之后仍然只涨不跌或者每次GC之后存活对象的大小都在缓慢爬升这时候dump分析就要上场了。定位问题之前先分清楚方向这和看病开方子是一回事得先确诊再下药。1.3 MAT在JVM生态工具链里处于什么位置JVM问题排查的工具其实不少jstat看实时GC情况jmap可以手动生成dumpjstack看线程VisualVM虽然有可视化界面但分析能力相对薄弱JProfiler是商业软件功能强但贵。MAT的全称是Eclipse Memory Analyzer最大的特点是免费、开源、分析速度快而且它对hprof格式的解析能力极强能处理几个GB的大dump文件。MAT的定位和Source Code分析工具不同它专门面向“对象驻留分析”。它能告诉你哪个对象占用内存最多、哪个对象被谁引用导致无法回收、类的实例数量分布是否异常。这一点是VisualVM这类工具很难替代的。我在实际项目里常规错误用VisualVM快速定位真正复杂的泄漏场景直接上MAT给证据链两套工具配合使用。2. 如何产出一份“合格”的dump文件拿到MAT之前先得保证你手里的原材料质量过关。你dump的时机不对或者命令参数用错了分析半天都是白费力气。dump文件的生成看似简单实际上有几个非常容易踩的坑我自己就曾经因为dump时触发了Full GC把一个泄漏现场给“掩盖”过去了。2.1 jmap命令的正确打开方式生成dump文件最标准的工具是JDK自带的jmap。如果Java进程的PID是12345基础命令长这样# 生成包括所有对象包含不可达但尚未被GC的对象的dump文件 jmap -dump:formatb,file/data/dump/heap_20240615.hprof 12345 # 只导出存活对象会先强制触发Full GC jmap -dump:live,formatb,file/data/dump/heap_20240615_live.hprof 12345注意区分这两个命令。-dump:live会先把Full GC跑一遍然后把存活对象导出。这在分析某些场景时反而是副作用如果问题本身就是某些本该被回收的对象被错误地引用住了还活着那没问题但如果你想分析的是堆的整体使用情况Full GC之后dump出来的是一个被“打扫”过的堆泄漏对象的引用链可能被GC影响问题现场就被破坏了。所以我通常采用的是第一条命令——不带live参数保留堆的真实状态让GC看着像什么就是什么。提示生产环境执行jmap对进程会有一定影响尤其在堆特别大的情况下dump过程可能导致应用短暂卡顿。建议在流量低峰期操作或者使用jmap -dump:live的替代方案详见2.3节。2.2 标准输出位置的路径陷阱生产环境的Linux服务器上默认当前用户可能没有写权限。很多新手直接在用户目录下执行jmap却把file路径写到了/root或者别的需要权限的目录最后报Permission denied白白错过问题现场。稳妥的做法是提前把dump目录建好比如mkdir -p /opt/app/logs/heapdump chmod 777 /opt/app/logs/heapdump jmap -dump:formatb,file/opt/app/logs/heapdump/$(date %Y%m%d_%H%M%S).hprof 12345还有一点不可忽视磁盘空间。一个6GB的JVM堆dump文件的体积通常会达到4-5GB甚至更多尤其是在堆碎片比较多的情况下hprof文件可能超过堆大小。生成之前先检查一下磁盘剩余空间是否充足否则dump到一半磁盘满了文件损坏这次抓取就废了。我见过有人把dump文件写到根目录/下面的直接导致服务器磁盘满告警这就是典型的“抢救病人先把自己搭进去”的场景。2.3 高版本JDK的新选择jcmd与自动转储JDK 8开始官方实际上更推荐使用jcmd来生成dump。命令形式更统一还能处理一些jmap搞不定的场景jcmd 12345 GC.heap_dump /data/dump/heap_20240615.hprofjcmd的GC.heap_dump默认行为也是不强制Full GC的和生产事故现场的堆状态更接近想要保住现场完整性的话推荐优先使用jcmd。另外在OOM真正发生之前自动留下dump文件是很多团队的标配。只需要在JVM参数里加上-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump/这个参数的意思是一旦发生OutOfMemoryErrorJVM自动在指定目录下生成dump文件。这个能力我强烈建议每个服务都开启因为线上OOM那一刻你是不在场的等你发现服务挂了跑过去再看现场早就没了。让JVM自己在崩溃瞬间拍下的“遗照”往往是最接近真相的。2.4 dump文件拿到手后怎么看它是不是好的一个“合格”的dump文件必须满足三个条件第一文件完整可解析MAT能正常打开第二dump时刻和问题高发时刻吻合——你要是服务都重启了才想起来dump那抓到的是重启后的健康堆屁用没有第三文件中包含了我们需要的引用链信息和GC Roots信息这就要求jmap参数必须带formatb用二进制格式这样MAT才能完整解析。验证文件是否能正常打开也有技巧文件下载到本地之后先看大小是否正常再用MAT打开之前可以用JDK自带的jhat保留工具快速测试一下不过jhat对比较大的文件非常吃力。更实用的办法是直接让MAT去解析解析不出错就说明文件结构和内容基本完好。3. MAT核心视图逐个拆解——从入口到结论的完整路径拿到dump文件之后第一次打开MAT的时候会觉得信息爆炸。那么多面板、那么多数字到底先看哪个这里直接按我日常分析的顺序把MAT的核心视图讲清楚你照着这个顺序走效率至少翻一倍。3.1 Overview概览页先看全局再看细节MAT打开dump文件后默认展示的是Overview概览页。顶部是全局图表展示堆总大小、类数量、对象数量和类加载器数量。页面上有若干快捷入口。我最先看的永远是饼图——最大的那个颜色块是不是异常的大。正常健康的堆最大的对象集合占总用量一般不会超过百分之二三十如果你发现单个对象集合占了堆总量的百分之五六十以上那基本就锁定了大方向。Overview上还有一个细节值得关注底部显示的“Class loader”数量。当服务使用了热部署或者动态代理大量生成类时类加载器数量可能异常膨胀。类加载器无法被卸载是方法区泄漏的典型症状这个问题在Overview页就能看出苗头。3.2 Leak Suspects自动报告机器智能和人工判断的配合点击工具栏上的“Leak Suspects”按钮MAT会根据对象大小和GC Roots的引用关系自动生成一份嫌疑报告。每个嫌疑项会展示占用的堆大小占比以及一条从GC Root到问题对象的引用路径。这个功能非常强大像是请了一个自动排查助手。但是要明确一点Leak Suspects是辅助工具它给出的“Suspect”只是基于大小和引用关系做的推测并不等于代码层面真正的逻辑缺陷。比如它可能检测到一个大HashMap占用了几百MB并且被某个静态变量持有于是把它列为头号嫌疑。但查完之后你发现这个HashMap其实是被一个缓存框架正常使用只是缓存key的设计有缺陷导致键不断地增长。所以MAT给你线索但真正的决断还得靠你对业务逻辑的理解。当嫌疑项里有多个时我的习惯是先把“被线程对象持有”的和“被类静态变量持有”的分类前者通常是局部变量逃逸或者线程池任务未清理后者通常是全局缓存或单例对象搞事两类问题的排查方向完全不同。3.3 Histogram类直方图从类的维度暴力定位点击“Histogram”按钮你会看到按类名分组的对象列表包含每个类的对象数量、Shallow Heap对象自身占用大小、Retained Heap对象及其持有的引用对象合计大小即可释放空间。Histogram的正确用法是用“Retained Heap”做降序排列。Retained Heap才是真正能被垃圾回收释放的内存量它等于这个对象连同它引用的子孙对象一起被回收时释放的总大小。你可以把Shallow Heap理解成一个人自己穿的衣服的重量Retained Heap则是他身上所有行李和背包装备都算上的总重量。举个例子你在Histogram里看到一个com.example.service.DataLoadWorker类Retained Heap有800MB但Shallow Heap只有几KB。这说明这个对象自身不大但它持有了一大堆子对象是个典型的“内存大户背后的引线”。顺着这个类的对象查看引用链通常很快就能定位到业务代码里的问题。3.4 Dominator Tree支配树理清“谁是谁的根”支配树可能是MAT里最需要理解的概念了。在对象引用关系中如果从GC Root到对象B的任意路径都必须经过对象A那么A就支配了B。整个对象的支配关系构成树结构就是Dominator Tree。这个视图的价值在于它能直接展现如果释放某个对象到底能释放多大比例的堆内存。比如你的dump文件里有100万个Order对象每个没多大但它们的支配链上都挂着一个共享的OrderConfig对象这100万个对象有可能因为持有了某个List引用导致整个市场配置对象全部无法回收。单看每个Order你不会觉得有什么问题但站在支配树视角OrderConfig这个节点下面挂着巨量的子孙对象它才是应该被关注的“承重墙”。Dominator Tree面板里每个对象都显示Retained Heap对这个数值排序前面节点的问题优先级最高重点看前三名的对象引用链即可。切忌从头到尾一个一个撸那是在浪费生命。3.5 OQL对象查询语言像写SQL一样查堆当图形化视图搞不定复杂查询场景时OQL是你手里的终极武器。MAT提供的OQL语法和SQL有一定相似之处能够按条件筛选对象集合。比如我要查一个从线程池提交的任务队列里有没有积压大量未被处理的任务SELECT * FROM java.util.concurrent.ThreadPoolExecutor比如我想看看某个特定类有多少个实例SELECT t, t.name, t.threadGroup FROM com.example.thread.WorkerThread AS t最常用的一条是查字符串池里是否有异常的重复字符串SELECT s.toString() AS val, COUNT(s) AS cnt FROM java.lang.String s GROUP BY val ORDER BY cnt DESC LIMIT 20这条OQL能快速发现“String对象大量重复”这类内存杀手。有些系统在循环里用字符串拼接并且没走常量池优化几百万个完全相同的字符串对象堆里一眼就能扫出来。3.6 Thread Overview线程视图别忽视正在跑的线程线程视图经常被人忽略但它在分析某些特定问题时价值不小。MAT能够展示dump时刻所有线程的状态、栈帧、持有的锁以及线程局部变量。排查线程池里ThreadLocal未清理导致的内存泄漏时Thread视图就是主战场。Java线程池里的核心线程是长期存活不销毁的如果代码往ThreadLocal里塞了对象但没调用remove清理这个对象就会挂在线程对象上跟随线程一直存活。随着任务处理越来越多ThreadLocal累积的对象就越来越大直到OOM。在这种场景下你需要在Thread视图里找到某个核心线程然后展开它的栈帧找到包含ThreadLocal的那一条局部变量链再顺着这条链往下看就能看到那个遗留的“垃圾”对象到底长什么样。4. 一个真实的OOM排查案例从dump定位到代码行的完整过程纯理论讲多了容易空洞这里把之前帮一个业务团队排查的线上OOM问题完整复盘一遍。这个案例相当有代表性几乎涵盖前面讲到的所有步骤。4.1 故障现象和初步判断服务部署了16台机器某天下午开始陆续有几个实例报OutOfMemoryError进程直接挂掉。日志里看到的是java.lang.OutOfMemoryError: GC overhead limit exceeded。服务重启后能稳定运行几个小时然后又开始报警周而复始。当时的初步判断是有内存泄漏但泄漏速度不算极快导致需要几个小时的积累才会打爆堆。GC日志显示每次Full GC耗时都在增加Full GC之后老年代占用也在缓慢增长从每次GC的40%逐步爬升到60%、75%、90%——完全符合“不可回收对象持续累积”的特征。于是决定在所有实例上开启HeapDumpOnOutOfMemoryError等待下次OOM时自动留下现场。4.2 dump文件的第一眼印象第二天其中一个实例再次OOM自动dump生成了文件大小3.8GB。用MAT打开后Overview的饼图先给了我一个直观信号——最大的那块是java.util.concurrent.ConcurrentHashMap$Node数组约占堆总量的47%。点开Histogram按Retained Heap排序排在前面的依次是类名对象数量Shallow HeapRetained HeapConcurrentHashMap$Node约240万64MB1.8GBbyte[]约180万760MB2.1GBString约120万48MB860MB直觉告诉我有个ConcurrentHashMap在持续累积无法清理。但Map本身在业务代码里被使用的场景太多了不可能直接定位到哪个业务数据。这时候需要用引用链来追溯。4.3 顺着Dominator Tree追根溯源进入Dominator Tree右键点击那个巨大的ConcurrentHashMap$Node选择“Path to GC Roots” - “exclude all phantom/weak/soft etc. references”把弱引用和软引用都排除掉只看强引用路径。路径逐渐清晰起来ConcurrentHashMap$Node - ConcurrentHashMap -com.example.gateway.filter.route.RouteCache- static对象。到这一步问题已经浮出水面了。去代码里翻RouteCache这个类果然是个单例里面定义了一个:public static final ConcurrentHashMapString, RouteConfig routeCache new ConcurrentHashMap(1024);业务逻辑里网关每接收到一个外部请求就会根据请求URL从路由表中加载对应的RouteConfig放入这个缓存。设计初衷是避免每次都重新加载路由配置思路没问题。但坑在细节里路由配置的key不是固定的比如包含时间戳、随机数等动态部分所以同一个业务路由的配置会以不同的key反复写入缓存而旧key永远不会被淘汰。每个路由配置对象又包含了上百条过滤器链、断言规则等子对象单个RouteConfig的Retained Heap就有十几KB。流量一天几百万缓存里的key滚雪球式增长就把堆打爆了。4.4 用OQL验证结论结论有眉目之后还得验证一下不能靠猜。用OQL对dump文件里的RouteConfig对象做了一次按key分组的统计SELECT r.key, COUNT(r) AS cnt FROM com.example.gateway.filter.route.RouteConfig r GROUP BY r.key ORDER BY cnt DESC结果一发出来就实锤了同样的业务URL对应的RouteConfig对象数量高达80多万个而key存在明显规律——只有最后一段ID不同前面完全相同。这就是典型的缓存key设计缺陷。4.5 修复方案的落地修复方案不复杂两件事第一给RouteCache加上淘汰机制设置最大容量和存活时间每次put的时候检查超阈值就清理过期条目。用现成的Caffeine或者Guava Cache就能实现没必要自己造LRU轮子。第二从源头上是把缓存key设计成稳定的路由规则散列值而不是把完整请求参数拼进来。同一个规则只缓存一份后续请求直接命中。修复上线后观察了一周Full GC频率从每小时好几次降到了每天一两次老年代内存占用一直平稳在30%左右问题彻底解决。5. MAT分析中的常见误区和高级技巧工具会用只是第一步用得对才能真正解决问题。下面这几个误区和技巧全是我在实战中踩出来的或者验证过的高价值内容。5.1 只看Shallow Heap不看Retained Heap很大概率找错方向Shallow Heap只统计对象自身字段占用的内存不包含它引用的子对象。对于String对象Shallow Heap相对较小但一个String持有的char[]字节数组可能非常大。对于集合类尤其是Map和ListShallow Heap完全无法反映真实占用量。真正的分析应该以Retained Heap为主排序指标。一个对象占用的总内存量应该用它及其所有引用对象的总和来衡量这正是Retained Heap的定义。被Retained Heap列第一的对象就是释放它之后能释放最多内存的对象优先排查它永远不会错。5.2 注意MAT解析大文件时的消耗策略MAT本身也是一个Java程序解析几个GB的dump文件时它自己也需要足够的内存。默认情况下MAT的JVM堆可能只有1GB打开3GB以上的dump文件会直接报OutOfMemoryError。打开MAT安装目录下的MemoryAnalyzer.ini文件调大-Xmx参数-startup plugins/org.eclipse.equinox.launcher_1.x.x.x.jar --launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.x.x.x -vmargs -Xmx4096m经验值是-Xmx设置为dump文件大小的两倍左右比较稳妥例如分析4GB的dump文件MAT内存设置为6GB甚至8GB。别设置太大否则你本机的内存也不够MAT分析时会疯狂做磁盘交换速度反而慢得令人发指。5.3 两份dump快照对比——找“增长趋势”比看“绝对大小”更准单看一份dump有时会误判。比如一个服务本身就要加载几百MB的配置数据这在业务上完全合理不一定是泄漏。这时候最高效的方法是对比两份不同时间点的dump文件。MAT提供了对比功能。先在分析报告中打开第一份dump再拖入第二份dump它会自动对比两份文件的Histogram和Dominator Tree差异。你可以直接看到哪个类的实例数在增长、哪个对象的Retained Heap在膨胀。这个能力的价值在于它能直接回答“哪些对象在这段时间内增加了”而增量对象才是真正值得关注的候选者。实际做对比时一定要注意两份dump的JVM参数和堆大小设置必须一致否则对比结果毫无意义。另外对比过程中间服务经历的流量压力差异也需要考虑进去流量高峰期的对象量增长是正常现象不代表泄漏。5.4 不同参考类型的影响——排除虚引用、弱引用和软引用干扰对象的回收和四种引用类型强引用、软引用、弱引用、虚引用关系密切。Java的软引用和弱引用对象在一定条件下是可以被垃圾回收器回收的它们不一定是泄漏元凶。使用MAT的“Path to GC Roots”功能时默认会把所有引用类型都算进来这就会产生误报——你顺着一条弱引用链找到了一个大对象但它在下次GC时就会别清掉根本不算泄漏。所以分析时选择“exclude all phantom/weak/soft etc. references”把非强引用全部排除只看那些真正“拽住”对象不放的强引用路径。这一点很多人图省事不看结果排查了半天找到的所谓“泄漏”对象其实是个可回收的弱引用白忙一场。强引用路径才指向真正的GC Root问题。5.5 ThreadLocal相关问题的一套固定排查手法线程池和ThreadLocal的搭配是生产环境内存泄漏的重灾区而且它隐蔽性极高——不翻Thread视图完全看不出来。排查套路其实很固定我这里直接给出来第一步在MAT线程视图里勾选某一个常驻核心线程第二步展开线程栈帧找到持有ThreadLocal的那一层第三步顺着ThreadLocalMap的Entry链往下展开第四步找到那个Entry的value值对象看它的Retained Heap大小和类名。如果value是一个业务对象或者一个巨大的List/Map再去看业务代码里有没有调用ThreadLocal.remove()。没有调用的基本就是这里泄漏了。修复方式也很简单finally块里必须remove()。这种问题在网上已经有很多人踩过坑但每次遇到都觉得防不胜防因为代码逻辑本身完全合理就差一步清理。5.6 版本适配和高版本JDK的注意事项日常使用MAT有一点需要注意的是版本适配问题。JDK 8时代生成的hprof文件格式和JDK 11、JDK 17下的格式有一些差异旧版MAT解析新格式的dump文件时会出现类信息丢失或者对象大小统计不准的问题。建议的做法是保持MAT的版本尽量新。如果用的是JDK 11最好直接用MAT 1.13以上的版本。另外GitLab CI或者容器化部署环境下jmap生成的dump文件形态会有细微差别比如JVM运行在容器内jmap获取PID要用容器内的进程号导出前先docker exec进容器确认一下PID别在宿主机上直接jmap容器内进程那种错误很低级但真实发生过。6. 结合JVM内存模型理解MAT分析结果分析dump文件不能只看工具的输出还应该把结果和JVM本身的运行机制结合起来思考。不懂底层原理你拿到一堆数据也不知道怎么看。6.1 对象在堆区各分代的流转按照JVM分代模型绝大多数对象先在Eden区创建经历Minor GC后存活对象进入Survivor区多次存活后晋升到老年代。dump文件虽然只拍下了堆的最终状态但对象所在的分代位置其实有迹可循——MAT的Histogram里Eden区对象会显示为不同的状态标记。如果发现dump里大量对象在Eden区就积压不下说明对象分配速率太高可能是代码里频繁创建大量临时对象。如果老年代对象持续膨胀且无法回收则很可能存在强引用链阻止GC——或者说有对象被GC Roots错误地“标记”为存活状态。这时候沿着MAT给出的GC Roots路径去分析就能定位是JVM参数里的某些设置放大了问题还是业务代码的引用持有导致老年代清不掉。6.2 垃圾回收器的选择会影响dump分析的关注点比较常见的影响点是CMS/Parallel GC下老年代泄漏的表现为Full GC频率上升而G1 GC因为它的Region模型和Mixed GC机制表现就更隐蔽通常是Mixed GC次数变多但整个堆的占用率仍然在慢慢上升。ZGC和Shenandoah这类新式回收器主打低延迟它们对泄漏的容忍度更低因为几乎没有完整的STW阶段来强行压缩堆。在分析dump时如果项目用的是G1要更关注Region的Humongous区域——超过Region大小一半的对象直接进入Humongous区这种大对象如果频繁出现且不释放会以超乎想象的速度碎片化堆空间。MAT中可以直接看大对象报告快速抓出那些体积巨大的对象这个在G1场景下特别实用。6.3 常量池、静态变量和类加载器的“隐形”内存堆分析里最常被忽略的一片区域就是静态变量持有的常量池和类元数据。类的静态字段是挂在GC Roots上的它引用的对象永远不会被回收。所以你如果看到一个类里定义了静态的List或者Map而且代码里还在往里面加东西那基本就是一个不会自动缩小的全局容器。类加载器泄漏则更隐蔽每次部署热更新或者动态生成代理类新的类加载器会加载新的类旧类加载器如果还被某个ThreadLocal的value引用着啊又是ThreadLocal就会连带它所加载的所有类一起滞留内存。MAT的Overview页看类加载器数量能快速发现这个问题线索进一步用Histogram按ClassLoader分组就能看到哪个加载器下面挂了一堆垃圾类。我个人的习惯是每份dump文件在手先看三个指标的总貌——堆总量、类加载器数量、老年代对象总量。任何一个指标远超同类型正常服务都不用再犹豫直接往对应方向深挖。7. 实战中的几个经验和最后的建议处理过的线上内存问题多了慢慢就会形成条件反射。这里分享几个关键时刻能救命的小经验如果对你有用那这篇文章的价值就达到了。7.1 排查内存问题时的最合理顺序效率最高的顺序是先看JVM参数配置然后看GC日志再接着看堆的实时趋势最后才决定要不要dump。一上来就dump是大材小用也很可能抓错现场。我见过无数人服务刚OOM就想dump但新手村最容易犯的错误是——服务已经重启完了才想起来dump已经晚了。如果进程还在但内存告急用jmap导dump保住现状再思考。如果进程已经崩了那就得依赖HeapDumpOnOutOfMemoryError留好的“遗照”。这两种情况下dump文件的分析方法论完全一样但数据的置信度差别很大——前者内存还没爆对象还能正常分配后者已经处在崩溃边缘对象分布可能存在和正常运行时不同的人为现象。7.2 这个分析思路还能用在哪些场景上MAT分析dump的能力不只能在OOM场景使用。比如排查服务启动后内存缓慢爬升比如排查某个功能上线后GC频率明显加大再比如定位缓存框架里key是否无限增长这些场景都可以dump一次看看。本质上任何“内存使用随时间不规律增长”的问题都值得用一张内存快照来看看到底是哪些对象在膨胀。还有一类使用场景容易被忽略线程池参数的合理化配置。通过dump文件里线程栈和线程局部变量的分析你能看清楚线程池里到底有多少任务在排队、任务对象有多大从而判断核心线程数和队列容量设置是否合理。这比拍脑袋配参数靠谱得多。7.3 最后再分享一个护身符级别的土办法线上排查系列问题做多了最后我习惯在每个服务启动参数里统一加上这两行-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/app/heapdump/然后配一个定时任务比如每2小时执行一次用jmap定期抓一份dump文件保留。这种做法不是每次都用得上但一旦线上发生OOM你既有崩溃瞬间的dump也有崩溃前正常状态的dump两份一对比找出增量对象问题基本就无处遁形了。很多团队出过一次OOM就把HeapDumpOnOutOfMemoryError打开了但打开之后从来不验证dump文件能不能正常落盘、目录空间够不够。所以我强烈建议开完参数之后手动触发一次jmap生成一个测试文件确认路径可达、文件可解析再去睡觉。这比你留着报警电话要踏实得多。
返回列表