图 1:本文封面,OOM 的第一反应不该是加内存。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。面试官的描述往往很生活化:线上 Java 服务跑了几天,突然开始报 OutOfMemoryError,你怎么查?这道题考的不是背参数,而是一条有顺序的排查链路。
先把结论立住:OOM 排查的第一步永远是定性——先分清溢出类型,再判断这是「泄漏」还是「堆确实不够大」,因为两类问题的解法完全相反:泄漏要修代码,不够大才调参数。上来就加内存,泄漏只会换个日子再爆。
**第一步,看报错的 detail message 分清类型。**OutOfMemoryError 后面跟的那句说明文字就是分类入口:Java heap space 是堆装不下对象,可能只是配置不足,也可能是真泄漏;Metaspace 是类元数据区域耗尽,方向完全在类加载与动态生成类上,和堆无关;GC Overhead limit exceeded 表示虚拟机把约百分之九十八的时间花在 GC 上却回收不到百分之二的堆,通常意味着存活数据已经贴着堆上限。另外栈溢出(StackOverflowError)虽然不是 OOM,也常被一起报上来,入口是线程栈深度而不是堆。类型看错,后面全是白费功夫。
**第二步,查 GC 日志,定性泄漏还是不够大。**把每次 Full GC 之后的堆占用连成线:如果回落的低点一次比一次高,像楼梯一样往上爬,这是内存泄漏的典型波形——每次回收都有东西留了下来;如果占用高但每次 Full GC 都能干净地降回低位,那更像堆配小了,或者业务瞬时创建了大对象。这一步不需要任何重型工具,GC 日志加一张图就够,但它决定了你接下来是改代码还是改参数。
**第三步,抓堆转储。**确认疑似泄漏后,对在线进程抓一份堆快照:jcmd <pid> GC.heap_dump <文件路径>,官方文档注明这个操作会先做一次 Full GC、开销随堆大小而定,抓之前要有预期。更稳的做法是未雨绸缪:启动参数加上 -XX:+HeapDumpOnOutOfMemoryError,JVM 在抛出 OOM 时会自动生成堆转储,官方说明这个选项运行时没有额外开销,适合生产环境常备;dump 路径可以用 -XX:HeapDumpPath 指定到独立磁盘,避免把系统盘写满。
**第四步,在分析工具里看支配树。**把堆转储加载进 Eclipse Memory Analyzer(MAT)这类工具,按保留堆大小(retained size)排序看支配树——支配树回答的问题是「如果这个对象被回收,连带能释放多少内存」。泄漏的典型特征一眼可见:某一个对象或集合一个人占掉大半堆。MAT 还能直接跑 Leak Suspects 报告,自动给出嫌疑对象。
**第五步,沿引用链向上追。**找到支配者之后,看它到 GC Root 的引用链,定位「是谁一直抓着这批对象不放」。反复出现的凶手就那几类:没有容量上限的本地缓存,只进不出;ThreadLocal 用完没清理,在线程池里线程不死、引用一直挂;静态集合只增不减,每次请求往里塞一点。
图 2:五步排查链路,先定性再动手。
**一个典型的定位过程长这样。**服务每隔几天 OOM 一次,GC 日志显示 Full GC 低点持续抬升——定性为泄漏;抓 dump 进 MAT,支配树顶端是一个静态 Map,占了堆的七成;沿引用链追到一个请求上下文对象,每次请求结束只删了部分键。修复动作是给这个 Map 加容量上限与过期淘汰,而不是调大 -Xmx——调大只是把爆发从三天推迟到五天。
**常见误区。**一是所有 OOM 一律加内存,泄漏被掩盖而非解决;二是把 GC Overhead limit exceeded 当成单纯堆不够,忽略它提示的是「回收效率已经崩了」;三是把 Metaspace 溢出当堆问题查半天 dump;四是把几个 G 的堆转储拖回办公电脑分析,正确的姿势是在有内存的机器上处理;五是只在出事时才想起抓 dump——平时没开自动转储参数,事故现场往往已经重启没了证据。
这套链路对 AI 服务同样适用:推理进程常驻大对象(模型缓存、批缓冲、会话历史),泄漏一点都是大块的。你最近一次内存排查,凶手是无界缓存、ThreadLocal 还是别的什么?欢迎分享。