面试现场面试官问了个很实在的问题。面试官假设线上服务突然抛了 OutOfMemoryError你手上只有一个 PID你怎么查候选人A重启一下——面试官手里的笔停了一下。候选人A赶紧补我是说重启之后加内存把-Xmx调大点。面试官如果调大之后过两天又 OOM 呢候选人A那就……继续调大面试官换了个候选人你呢你怎么查候选人B先看报的是哪种 OOM。Java heap space是堆、Metaspace是元空间、Direct buffer memory是直接内存这几种现场和排法完全不同。如果是堆先用jmapdump 下来丢进 MAT 看支配树找是谁在占内存同时用jstat -gcutil盯 GC 频率和曲线看是不是回收不动。查的时候心里要有个判断——是内存泄漏该回收的没回收还是内存本来就不够。前者要改代码找引用链后者才是调参数。面试官那你为什么先 dump 而不是先重启候选人B重启就没有现场了。现场比什么都值钱。今天这篇就是把这套排查流程完整走一遍。理论前面讲了三篇这篇全是手上有把螺丝刀怎么拆。一、先认清敌人OOM 长什么样几种OutOfMemoryError 有好几种报错信息里那句冒号后面的字直接决定了你往哪个方向查。最常见的几种-Java heap space——堆溢出。对象太多、内存泄漏、或者堆本来就不够。-Metaspace——元空间溢出JDK 8。类元数据太多典型是动态生成类。-PermGen space——永久代溢出JDK 7 及以前才有JDK 8 之后没了。-Direct buffer memory——直接内存溢出。NIO / Netty 场景的常客。-unable to create native thread——创建不了线程名字像栈其实是系统资源不够。-GC overhead limit exceeded——GC 干了太多活却没收获。默认当 GC 时间占比超过 98%、而回收出来的空间不足 2% 时抛出由-XX:GCTimeLimit、-XX:GCHeapFreeLimit控制。本质上是堆快满了但 GC 已经无能为力。还有一类不是 OutOfMemoryError但排查时老一起出现——StackOverflowError栈溢出一般发生在递归太深的时候。记住这张表能省你一半的瞎查时间。看到Direct buffer memory却去 dump 堆方向从一开始就错了。二、先分清内存泄漏还是内存不够这一步很多人跳过直接上手调-Xmx结果要么治标不治本要么白调。内存泄漏对象明明不该被引用了却因为某个引用链一直挂着GC 回收不掉。它的特征是——内存随时间一路往上涨老年代占用越来越高Full GC 越来越频繁但每次 GC 之后老年代的水位几乎不下降。这种你调多大堆都没用早晚撑爆必须改代码。内存本来就不够业务数据量确实大堆就是不够用。它的特征是——加了内存之后问题明显缓解甚至消失属于容量规划的范畴。怎么区分看两次 Full GC 之后老年代的水位。如果 Full GC 前 90%、Full GC 后还是 90%那就是泄漏——回收根本收不动。如果 Full GC 前 90%、Full GC 后掉到 40%那多半是容量不够或者流量突发。还有个更省事的办法启动时加上-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/your/path。这样一旦 OOMJVM 自动把堆 dump 下来你连赶在重启前 dump这一手都省了。生产环境强烈建议默认带上。三、工具箱五个命令各管一段JDK 自带的那几个命令行工具排查 OOM 就靠它们。先把各自管什么记住。jps列出当前机器上的 Java 进程和 PID。排查第一步先定位进程号。jstat看 GC 统计。最常用的是# 每 1000ms 打印一次 GC 统计共打印 10 次 jstat -gcutil pid 1000 10输出里的列要能看懂S0、S1是两个 Survivor 区的使用率E是 EdenO是老年代M是元空间JDK 8 里是MJDK 7 是PYGC/FGC是年轻代、Full GC 的次数YGCT/FGCT/GCT是对应的耗时。你盯着一行行刷看O这个老年代使用率有没有持续上涨、FGC涨得快不快基本就能判断压力在哪。jmap堆的透视镜三个用法最重要jmap -heap pid # 看堆的配置和使用情况 jmap -histo:live pid | head -30 # 对象直方图哪类对象最多、最占内存 jmap -dump:live,formatb,fileheap.hprof pid # 把堆 dump 下来-histo输出的是类 → 实例数 → 占用字节的排行一眼能看出是不是某个类的实例多到离谱——比如几百万个UserDto那问题基本就在它身上。-dump输出的.hprof文件是给 MAT 分析用的。有个坑要记带live的-histo:live和-dump:live会先触发一次 Full GC再统计存活对象。这能过滤掉垃圾但也意味着——它会 STW对线上是有影响的。所以这两条命令在生产环境上要谨慎最好留到低峰期或者干脆用-XX:HeapDumpOnOutOfMemoryError在 OOM 那一刻自动 dump。jstack打印线程栈。jstack pid stack.txt它主要用来抓线程状态——死锁、线程卡在哪、某个线程栈有多深。排查创建不了线程和栈溢出的时候要用它。jinfo看运行时参数比如某次 JVM 到底带了哪些-XX。排查前先确认参数能避免很多我以为它是这么配的。想更顺手的话可以上Arthasdashboard一目了然、heapdump导堆、thread看线程或者VisualVM它们把上面这些命令包成了可视化界面。但底层原理就是这几个命令面试时说出命令名和参数比只说用工具看一下有说服力得多。四、堆溢出dump 下来丢进 MAT堆 OOM 是最高频的排查流程也最标准。第一步确认现场。用jstat -gcutil看老年代O是不是居高不下、Full GC 之后也不掉FGC是不是在短时间内猛涨。如果是基本坐实了堆的问题。第二步拿到 dump。要么用jmap -dump:live,formatb,fileheap.hprof pid手动抓要么靠-XX:HeapDumpOnOutOfMemoryError自动抓。第三步MAT 分析。MATEclipse Memory Analyzer是分析 hprof 的主力工具。打开 dump 之后重点看三个地方-Histogram直方图按类统计实例数和占用找出哪类对象最多。它和jmap -histo是一个意思但 MAT 里能展开看具体实例。-Dominator Tree支配树这是 MAT 最有价值的视图。它按如果我释放了 A能连带释放多少内存来排序也就是看谁真正占着大头。直方图告诉你哪个类多支配树告诉你谁真正霸占了内存——一个类实例多但如果它们都被别的东西引用着那真正的元凶是那个引用者。找内存泄漏看支配树比看直方图准得多。-Leak Suspects泄漏嫌疑MAT 的自动报告会先把最可疑的几个内存块圈出来给你省时间。第四步找引用链。锁定了可疑对象就对它做Path to GC Roots——看它到底是从哪个 GC Root 一路引用过来的。这一步是定位泄漏源头的关键。查的时候记得排除弱引用和软引用exclude weak/soft references因为软引用、弱引用本身不会阻止回收被它们挂着不算泄漏排除掉才能暴露出真正的强引用链。举个例子排查下来经常看到这样的链条某个对象被一个 static 的Map引用着这个 Map 只往里放、从来不清理——典型的缓存没设上限或者监听器注册了没注销。找到这条链问题就解决了一半。五、非堆的三种 OOM元空间、栈、直接内存剩下三种 OOM 不在堆上光 dump 堆是查不出来的得换个思路。元空间溢出Metaspace特征是jstat里M这一列一直涨、GC 也不掉。根源多半是动态生成类——CGLIB 代理、大量反射、字节码增强框架、热部署。查的方向是谁在不停造类可以配合jmap -clstats pid看已加载的类统计或者加-XX:TraceClassLoading把类加载过程打出来。解药通常是给元空间设上限-XX:MaxMetaspaceSize但这只是兜底真正的修复是找出那个无限造类的代码。栈溢出 / 线程创建失败StackOverflowError用jstack一看就知道——某个线程的栈深得离谱通常是递归没有终止条件。而unable to create native thread要看的是是不是线程数开太多线程池没设上限、或者每个请求 new 一个线程也可能是操作系统层面的线程数、句柄数限制。这时候jstack数一下线程数量、ulimit看一眼系统限制方向就有了。直接内存溢出Direct buffer memory最阴的一种。它的特征是——堆看起来很正常机器物理内存却在飙升。DirectByteBuffer对象在堆里很小真正的大内存在堆外。jstat看堆一切正常但进程的 RES 内存涨得吓人。查的方向确认-XX:MaxDirectMemorySize是怎么配的不显式配的话默认跟-Xmx挂钩检查是不是 NIO / Netty 用得多但DirectByteBuffer回收不及时。想让 JVM 把本地内存的去向打出来可以用-XX:NativeMemoryTrackingsummary再用jcmd pid VM.native_memory summary看。这种问题光看堆的 GC 日志会被骗过去。整张流程图捋下来就一句话先看错误类型定方向 → 用 jstat 看趋势判断是不是泄漏 → 该 dump 的 dump、该 jstack 的 jstack → 用 MAT / 引用链定位到具体代码 → 改代码参数只是兜底。 面试官真正想听的答案问他 OOM 排查他想听的不是加内存、重启也不是工具名背诵。他想知道你有没有一套能落地、有判断、有条理的方法。第一层给结论查 OOM 第一步是看错误类型定方向——Java heap space查堆、Metaspace查类加载、Direct buffer memory查堆外、unable to create native thread查线程数。第二步用jstat -gcutil看趋势判断是内存泄漏还是容量不够。第三步按区域选工具堆用jmapdump MAT 看支配树和引用链元空间和直接内存要另查。工具就那五个——jps、jstat、jmap、jstack、jinfo。第二层讲判断力能区分内存泄漏和内存不够是这条的分水岭。泄漏看Full GC 后老年代水位不降要改代码不够看加内存就缓解是容量规划。另外生产环境第一原则是先保现场再动手重启之前先把 dump 拿到——所以-XX:HeapDumpOnOutOfMemoryError应该默认开着。第三层用工具细节证明你真用过jmap -dump:live会先触发一次 Full GC、有 STW 影响所以线上要谨慎MAT 里看支配树比看直方图准因为支配树才是真正占内存的那个找引用链时要排除软引用和弱引用直接内存 OOM 的特点是堆正常、机器内存爆得用 NativeMemoryTracking 才看得见。能把这些细节讲出来面试官就知道你是真的排过线上问题而不是背了篇文章。下篇预告OOM 排查讲完了下一篇聊 JVM 调优——参数到底怎么配。堆开多大、新生代老年代怎么分、-Xms和-Xmx该不该设成一样、GC 日志怎么看、延迟敏感和吞吐敏感的服务该怎么选收集器和参数。到时候会把一套从零配置一个服务 JVM的思路走一遍先定堆大小再选收集器再调停顿目标再用 GC 日志验证。不讲玄学参数只讲每个参数在解决什么问题。想预习的话把-Xms -Xmx -Xmn -Xss -XX:MetaspaceSize -XX:MaxMetaspaceSize -XX:SurvivorRatio这几个先过一眼。唠点键盘之外的 · 第 26 篇