线上 OOM 之后,第一时间先保哪些现场?
这是 OOM 现场保护页:最怕的不是暂时没结论,而是现场先没了,先分 heap、容器、堆外和线程分支,再按日志、Heap Dump、GC、线程、容器事件与业务时间线的顺序留证。
线上出现 OOM,最容易发生的不是“大家不会排查”,而是 真正该留的现场,在最开始几分钟里就被弄丢了。
这类事故现场很典型:
- 服务已经在报错或频繁重启
- 大家着急先恢复可用性
- 有人先扩容
- 有人先重启
- 有人先回滚
- 有人开始贴异常截图
这些动作很多时候都合理,尤其是救火阶段。但如果没有一个基本顺序,后面排查就很容易变成:
- 只知道报过 OOM
- 不知道到底是哪一种 OOM
- 不知道堆是怎么涨上去的
- 不知道线程、GC、流量、发布、容器状态当时是什么样
- 最后只能靠经验猜
真正麻烦的不是“OOM 了”,而是:
你明明出过一次大问题,却没把足够证据留下来,结果下次还会以差不多的方式再来一遍。
这篇就盯现场保护这件事:线上 OOM 之后,第一时间最值得保留哪些证据,哪些动作会让证据直接消失。
如果只想先记住一句话,可以用这个顺序:
先定 OOM 类型,再留日志和时间线;能保堆就先保堆,能保线程就补线程;同时把 GC、容器、流量、发布和依赖状态一起留住,最后再做恢复动作。
OOM 已经发生时,先保现场
先用这张表对位当前现场,判断是不是已经进入“现场保护优先”的阶段。
| 你现在看到的现象 | 更像什么 | 下一步 |
|---|---|---|
应用已经报 OutOfMemoryError,实例还可能继续重启 | 现场保护优先 | 按本文把日志、Dump 和时间线保住 |
Pod 被 OOMKilled,担心旧容器日志和 RSS 曲线马上消失 | 容器现场保护优先 | 按本文保住旧容器线索,再接 容器 OOMKilled 和 Java heap OOM,先怎么区分? |
| 现在还只是内存上涨、Full GC 变密,还没真正报 OOM | 更像前兆判断 | 先看 Java 内存持续上涨但没有 OOM,先怎么排查? |
| 你已经确认是 heap OOM,下一步想继续分类型和看对象 | 已经转入 heap OOM 深挖 | 接 Java 服务 OOM 了怎么排查?先分类型,再决定往哪条线查 |
| 你怀疑真正该保的是 RSS、线程数、堆外和容器事件 | 更像堆外 / 容器分支现场 | 接 堆外内存上涨把容器逼死时,先留哪些证据? |
这篇适合什么时候打开
OOM 已经发生、日志和容器线索还没彻底丢,但你还来不及深挖根因时,就先看这里。要是现在还只是内存上涨、Full GC 变密,先回前兆判断;要是已经确认进入 OOM 事故,就先照本文把日志、Dump 和时间线抓住。
它更像事故刚爆出来时的留证清单,不负责直接给根因下结论,而是帮你在最短时间里保住后续分析所需的证据。
一、OOM 现场最怕的,不是结论慢,而是证据先没了
如果你现在就在救火,第一轮优先保完整异常栈、实例或 Pod 标识、重启前后的 GC 日志、容器事件、Heap Dump / hs_err / 线程快照有没有落盘,以及业务侧开始报错的时间点。
很多 OOM 排查之所以低效,不是因为工具不够,而是因为一开始做了下面这些动作:
- 直接重启实例
- 容器被平台自动拉起,旧容器日志很快被覆盖
- Heap Dump 没配置,重启后堆现场直接消失
- 只截了一张报警图,没有保留原始日志和时间点
- 只知道“内存不够”,不知道是 heap、metaspace、direct memory 还是线程问题
所以 OOM 之后第一件事,不是立刻下结论,而是先保护现场。
至少要先回答:
- 是哪一种 OOM
- 发生在哪个实例
- 发生前后内存、GC、线程、流量是什么状态
- 有没有 Heap Dump
- 有哪些临时动作已经做了
如果这些基础信息一开始没留住,后面即使看再多代码,也很容易一直停在猜测层。
二、第一步先定 OOM 类型,不要把所有 OOM 都当成 heap 不够
这是最关键的第一步。
你需要先拿到完整异常,而不是只记得“有个 OOM”。
常见的几类包括:
java.lang.OutOfMemoryError: Java heap spacejava.lang.OutOfMemoryError: GC overhead limit exceededjava.lang.OutOfMemoryError: Metaspacejava.lang.OutOfMemoryError: Direct buffer memoryjava.lang.OutOfMemoryError: unable to create new native thread
为什么这一步必须最先做
因为这几类 OOM 后面的排查路径完全不一样。
如果是 Java heap space
更要优先保:
- Heap Dump
- GC 日志
- 堆内存曲线
- 业务大对象、缓存、批任务线索
如果是 Metaspace
更要优先保:
- 类加载相关日志
- 发布和动态代理变化
- 类加载器、热加载、插件化线索
如果是 Direct buffer memory
更要优先保:
- 直接内存相关指标
- Netty / NIO 使用情况
- 堆外内存曲线
- 连接数和网络流量状态
如果是 unable to create new native thread
更要优先保:
- 线程数趋势
- 线程池监控
jstack- 容器 / 系统线程限制配置
所以 OOM 现场第一条纪律就是:
先把完整异常文本留住,再决定往哪条线深挖。
三、第二步先留时间线:哪一刻开始出问题,前后发生过什么
很多 OOM 后面难查,不是工具不够,而是时间线太模糊。
所以第二步建议先把最基础的时间线留出来:
- 第一次报警时间
- 第一次错误日志时间
- 实例第一次重启时间
- 如果有发布,最近一次发布时间
- 如果有批任务,任务开始时间
- 流量峰值或异常流量开始时间
为什么这一步这么值钱
因为很多 OOM 根因和时间窗口高度相关。
例如:
- 发布后立刻出现,更像新代码、参数或依赖变化
- 每天固定时间出现,更像批任务、报表或导出
- 高峰期才出现,更像流量、对象制造或容量边界问题
- 跑一段时间后才出现,更像泄漏、缓存膨胀或线程累积
后面不管你是查 Heap Dump、线程还是 GC,这条时间线都会帮你把方向收窄很多。
四、第三步优先保日志:不只保异常,还要保前后的上下文
很多人说“日志有啊”,但真正排 OOM 时,光有那一行 OOM 并不够。
更值得保留的是:
- OOM 前后 5-10 分钟完整应用日志
- JVM / 容器标准输出
- 平台事件日志(容器重启、探针失败、OOMKilled)
- 关键中间件和依赖异常日志
重点要看哪些内容
1. OOM 前有没有明显业务异常
例如:
- 某类大请求突然增多
- 某类报表、导出、批任务启动
- 某类对象序列化异常重
- 某个接口超时重试放大
2. OOM 前有没有 GC 异常征兆
例如:
- Full GC 越来越密
- GC overhead 相关日志
- 停顿明显变长
3. OOM 后平台做了什么
例如:
- 容器是否被 OOMKilled
- 健康检查失败导致是否被重启
- K8s / 容器平台是否拉起了新实例
很多时候,应用自己抛了 OOM 和容器因为内存超限被系统杀掉,排查重点并不完全一样。
五、第四步能保 Heap Dump 就优先保 Heap Dump
如果 OOM 类型偏向堆内存,Heap Dump 往往是最值钱的现场。
最理想的情况
服务启动时已经配置:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps
这样一旦发生堆 OOM,JVM 会自动把现场 dump 出来。
为什么这个现场特别重要
因为它能回答:
- 谁占了内存
- 哪类对象异常多
- 哪条引用链在把对象一直留在堆里
而这些信息,在实例重启后往往就很难再还原。
如果还没来得及配置,现场还活着怎么办
如果实例还没彻底挂,并且现场允许,可以再评估是否手动保留 dump:
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
但这里要注意:
- dump 是重动作
- 会占磁盘
- 高压场景下要先评估是否值得马上做
哪些场景下 Heap Dump 价值最高
Java heap spaceGC overhead limit exceeded- 你已经看到 Full GC 回不下去
- 内存基线长期上升
如果是 unable to create new native thread 这类问题,Heap Dump 价值就不是第一优先级了,这时线程现场反而更重要。
六、第五步补线程现场:尤其是线程数、阻塞和高 CPU 同时异常时
很多 OOM 现场只盯内存,但其实线程状态也很关键。
特别是下面这些场景:
unable to create new native thread- OOM 前线程数持续上涨
- 接口超时、线程池堆积和 OOM 一起出现
- OOM 前 CPU 也明显升高
这时更值得保的证据
- 线程总数趋势
- 关键线程池监控
jstacktop -Hp配合线程栈
例如:
jstack <pid> > /tmp/jstack-oom.log
如果时间允许,建议连续抓 2-3 次。
为什么线程现场不能漏
因为很多内存问题并不是单独出现的,它会和下面这些现象一起串起来:
- 线程堆积
- 大量请求挂住
- 下游超时重试
- 本地线程池不断扩张
- ThreadLocal 或上下文对象长期挂在线程上
如果只留内存,不留线程,后面很可能解释不清“为什么对象会一直活着”或者“为什么进程内存涨得这么快”。
七、第六步把 GC 现场一起留住:次数、停顿、回收效果都要看
OOM 前很多服务都会经历一段明显的 GC 异常期。
所以除了异常日志和 dump,还应该尽量保留:
- GC 日志
jstat输出- GC 监控图
最值得保哪些信息
- Young GC / Full GC 次数
- Full GC 发生频率
- 每次回收后 Old 区是否明显下降
- 是否已经出现
GC overhead limit exceeded
如果实例还在,可以快速补一段:
jstat -gcutil <pid> 1000 10
为什么 GC 现场很重要
因为它能帮你回答:
- OOM 是突发打爆,还是长期恶化
- JVM 还能不能回收动
- 更像对象来得太猛,还是对象根本回不去
这对于后面区分:
- 内存泄漏
- 缓存膨胀
- 批任务冲击
- 堆配置不匹配
非常关键。
八、第七步别漏掉容器和系统层信息:很多 OOM 不是 JVM 自己先抛出来的
如果服务跑在容器里,这一步非常容易被忽略。
因为很多时候,你以为是“Java OOM”,实际是:
- 容器内存超限被 OOMKilled
- 平台健康检查失败触发重启
- sidecar、page cache 或其他进程一起挤占内存
所以还要一起保这些信息
- Pod / 容器重启原因
OOMKilled事件- 容器 memory limit / request
- 宿主机剩余内存和 swap 状态
- 同机其他进程是否也在抢内存
为什么这一步重要
因为:
- JVM heap 没打满,不代表容器不会先死
- 容器被杀和 Java 主动抛 OOM,现场残留和后续排查路线不同
- 如果只看应用日志,可能会误把容器资源边界问题当成纯代码问题
九、第八步把业务侧上下文一起留住:流量、参数、批任务、发布不能只靠回忆
很多 OOM 最终并不是单纯 JVM 层问题,而是业务侧触发条件非常明确。
所以建议同时保:
- 当时的流量曲线
- 错误率和 RT 曲线
- 关键接口 QPS
- 是否有大请求或异常参数
- 批任务 / 报表 / 导出 / 同步任务状态
- 最近发布、配置变更、依赖升级
为什么这部分特别值钱
因为后面很多判断都依赖它:
- 高峰期才触发,更像容量或对象制造问题
- 固定任务窗口触发,更像批处理和大对象问题
- 发布后立刻触发,更像代码或参数变更
- 某个接口单独放大,更像特定对象路径或缓存问题
如果这些信息不留,后面就只能靠“我记得那会儿好像有个任务在跑”这种不稳定记忆。
十、一个更实用的现场保留 checklist
如果线上现在就出了 OOM,可以按这个顺序做。
第 1 步:先定 OOM 类型
保留完整异常文本:
Java heap spaceGC overhead limit exceededMetaspaceDirect buffer memoryunable to create new native thread
第 2 步:先记时间线
保留:
- 第一次报警时间
- 第一次错误时间
- 重启时间
- 最近发布时间
- 批任务 / 高峰流量时间
第 3 步:保日志
至少保留:
- OOM 前后 5-10 分钟应用日志
- 平台 / 容器事件日志
- GC 日志
第 4 步:能保 Heap Dump 就优先保
尤其是 heap OOM 场景。
第 5 步:补线程现场
尤其是线程数异常、CPU 高、线程池堆积或 native thread OOM 场景。
第 6 步:保 GC 与资源曲线
- 堆
- Old 区
- GC 次数
- 进程 RSS
- 线程数
- CPU
- 容器 memory
第 7 步:保业务上下文
- 流量
- RT
- 错误率
- 批任务
- 发布和配置变更
十一、几个最容易让现场直接失真的动作
1. 还没保任何证据就先重启
有时候为了恢复服务必须这么做,但要清楚:
- 一旦重启,很多现场就回不来了
- 所以至少先把日志、异常文本、容器事件和时间线保住
2. 只截图,不留原始日志
截图能看个大概,但后面做精确判断时,原始日志、原始时间戳和完整错误文本更关键。
3. 只盯 JVM,不看容器与平台事件
容器场景里,很多内存事故不是 JVM 先说自己不行,而是平台先把它杀了。
4. 只保 Heap Dump,不保流量和发布信息
Heap Dump 很重要,但如果没有时间窗口和业务上下文,很多对象为什么会突然变多,还是解释不完整。
十二、FAQ:OOM 之后最常被问到的几个问题
1. 服务已经在重启了,还来得及保什么?
至少先保完整异常文本、容器 / 平台事件、OOM 前后日志、监控时间线和是否已落盘的 Heap Dump。就算实例马上没了,这些信息也能先把后续方向定住。
2. 不是堆 OOM,也需要优先保 Heap Dump 吗?
不一定。heap OOM 时 Heap Dump 非常重要;native thread OOM 更看线程现场;direct memory 或容器 OOMKilled 更看 RSS、容器事件和堆外指标,所以保现场一定要先分类型。
3. OOM 后必须马上抓 jstack 吗?
如果实例还活着,并且线程数异常、CPU 异常、线程池堆积明显,建议补;但如果容器已经先被杀,平台事件和旧容器日志往往比 jstack 更紧急。
4. 只看监控不看日志行不行?
不行。监控告诉你趋势,日志告诉你具体 OOM 类型和业务上下文,两者缺一块都会让后面的结论很虚。
5. 为什么 OOM 现场一定要留“发布时间”和“批任务时间”?
因为很多 OOM 根因都和时间窗口强相关。不把这些时间点和内存曲线、GC 曲线对齐,后面就很容易一直在错方向上兜圈子。
6. 这篇和 OOM 分类型那篇该怎么分工看?
分类型那篇负责先把 heap、metaspace、direct memory、native thread、容器分支选对;本文负责在你还没深挖前先把日志、Heap Dump、GC、线程、容器事件和业务时间线保住。前者帮你别走错方向,后者帮你别把现场丢掉。
十三、把现场留住后,后面的排查通常会分成几条线
我自己一般按手里最缺的那段证据往下接:
- 已经确认是 OOM 事故,想先把类型分清:
Java 服务 OOM 了怎么排查?先分类型,再决定往哪条线查 - Pod 被
OOMKilled,最担心旧容器日志和 RSS 线索一起丢:容器 OOMKilled 和 Java heap OOM,先怎么区分? - 怀疑真正该盯的是堆外、线程数和容器总占用:
堆外内存上涨把容器逼死时,先留哪些证据? - 回头一看,出事前其实已经经历过内存持续上涨和 Full GC 变密:
Java 内存持续上涨但没有 OOM,先怎么排查? - 错误是
GC overhead limit exceeded,想先确认是不是 GC 风暴中的中间态:GC overhead limit exceeded怎么排查?它常常不是普通 OOM - 证据已经留得差不多,准备继续盯堆对象和引用链:
Java 内存泄漏怎么定位?从 Heap Dump 到 MAT 实战
十四、回到现场保护本身
很多 OOM 最后难复盘,不是因为技术太难,而是因为第一轮现场处理只顾恢复,没有把关键证据留下来。
所以更实用的主线应该是:
先分 OOM 类型,再保时间线和日志;能保 Heap Dump 就保 Heap Dump,能补线程和 GC 就尽量补;同时把容器、流量、发布和批任务上下文一起留下来,最后再去做恢复和深入分析。
只要这个顺序不乱,OOM 就不会再只是“一次已经过去的事故”,而会变成一套可复盘、可收敛、也更容易避免复发的工程问题。