OOM 引起原因以及如何排查?

#Java#面试八股

OOM 引起原因以及如何排查?

结论先行:OOM 不是一种错误,而是一族错误。先看异常消息的后缀定位区域(Java heap space / Metaspace / Direct buffer memory / unable to create new native thread),再看是泄漏还是容量不足:Full GC 后占用不降是泄漏,降了就说明分配速率或上限有问题。排查的黄金三件套是 GC 日志、堆转储(heap dump)、分配热点,缺一个都只能靠猜。

一、按消息定位区域

OOM 消息区域常见原因
Java heap space堆泄漏、一次加载超大文件、缓存无上限
GC overhead limit exceeded堆98% 时间在 GC 却回收不到 2%,堆几乎被存活对象占满
Metaspace元空间动态生成类、热部署、CGLIB 代理膨胀
Compressed class space元空间(压缩类指针区)类数量过多,默认上限 1G
Direct buffer memory堆外Netty 未释放、allocateDirect 无节制
unable to create new native thread线程栈线程数触顶、ulimit -u 限制、每请求一线程
Requested array size exceeds VM limit堆单次申请接近 Integer.MAX_VALUE
Out of memory: Kill process操作系统容器 cgroup 限制,JVM 被内核杀掉(137)

二、排查流程

告警:OOM / 频繁 Full GC
   │
   ├─ 1. 进程还在吗?
   │     ├─ 在 → jmap/jcmd 导出堆,jstat 看趋势
   │     └─ 不在(137)→ 查 dmesg 与容器事件,确认是否 cgroup OOMKilled
   │
   ├─ 2. 异常消息指向哪个区域?
   │     ├─ 堆 → 看 dump,找最大保留集对象
   │     ├─ 元空间 → jcmd GC.class_stats / 看动态类数量
   │     ├─ 直接内存 → NMT(Native Memory Tracking)
   │     └─ 线程 → jstack 数线程,按栈顶归类
   │
   ├─ 3. 是泄漏还是不够用?
   │     └─ Full GC 后老年代是否回落 → 不降=泄漏,降了=分配过快/上限小
   │
   └─ 4. 定位到代码 → 复现 → 修 → 加监控

三、现场保留与取证

# 启动即配置,事故时才有现场
java -Xms4g -Xmx4g \
     -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/oom.hprof \
     -XX:+ExitOnOutOfMemoryError \
     -Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filesize=50m,filecount=5 \
     -jar app.jar

# 存活进程主动取证
jcmd 1234 GC.heap_dump /tmp/live.hprof
jmap -histo:live 1234 | head -30          # 对象数量与占用 Top30
jcmd 1234 Thread.print > /tmp/threads.txt # 线程栈,看线程数是否异常
jcmd 1234 VM.native_memory summary        # 需先加 -XX:NativeMemoryTracking=summary
// 最小复现:一个只进不出的本地缓存
public class OomRepro {
    private static final java.util.Map<String, byte[]> CACHE = new java.util.HashMap<>();

    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            CACHE.put("k" + i, new byte[1024 * 1024]);   // 每秒增长 1MB,永不淘汰
        }
    }
}
// 运行:java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./oom.hprof OomRepro

四、用 MAT 找「谁在占内存」

打开 dump 后按这个顺序看:

视图看什么结论形态
Histogram按类统计实例数与浅堆哪个类实例异常多
Dominator Tree按保留堆(retained heap)排序哪个对象真正压住了内存
Path to GC Roots排除弱引用/软引用后的引用链谁持有它、为什么回收不掉
Leak Suspects自动生成的疑似泄漏报告快速定位大块占用

五、常见追问

追问回答要点
dump 文件太大导不出来怎么办先 jmap -histo:live 看趋势,必要时对单机限流后导;也可用 jcmd GC.heap_dump 的 -all 参数排除不可达对象
生产不能重启怎么取证Arthas heapdump、jmap 在线导出;注意 dump 会触发 Full GC 造成秒级停顿
元空间 OOM 怎么查-Xlog:class+load 看类增长、jcmd GC.class_stats 看类加载器分布,重点查热部署与动态代理
堆外泄漏怎么定位NMT 的 Internal / Other 项持续增长;或 -XX:MaxDirectMemorySize 卡死后看 DirectByteBuffer 的 Cleaner
为什么加大 -Xmx 有时反而更糟停顿变长、单次 GC 更贵,且泄漏仍在,只是推迟了爆发时间

小结

OOM 排查是一条固定流水线:看消息定区域 → 看回收率定性质 → dump 找持有者 → 改代码或改容量。真正决定效率的不是工具,而是有没有在启动参数里留下现场。

笔记加载中…