ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

线上 OOM 排查完全指南(多工具命令版)

线上 OOM 排查完全指南(多工具命令版) 线上 OOM 排查完全指南多工具命令版线上 OOM 不是单一问题而是一类问题的统称。排查的核心思路是先分清是哪种 OOM再保留现场、收集证据、定位根因、修复验证。本文覆盖所有主要 OOM 类型每一类都给出错误信息、不同工具的具体命令、输出示例、判断依据、深入分析、常见根因和代码定位方法。常用工具包括jstat、jmap、jstack、jcmd、Arthas、MAT、dmesg、kubectl。一、先分清是哪种 OOMJava 应用的 OOM 有很多种错误信息不同排查方向完全不同。OOM 类型典型错误信息核心原因Java 堆内存溢出java.lang.OutOfMemoryError: Java heap space堆内对象太多、内存泄漏、大对象GC 开销超限java.lang.OutOfMemoryError: GC overhead limit exceededGC 频繁但回收效果差堆太小或泄漏元空间溢出java.lang.OutOfMemoryError: Metaspace动态生成类过多、热部署、CGLIB压缩类空间溢出java.lang.OutOfMemoryError: Compressed class space类元数据太多Metaspace 子区域不足直接内存溢出java.lang.OutOfMemoryError: Direct buffer memoryNIO/Netty ByteBuf 未释放、堆外缓存无法创建线程java.lang.OutOfMemoryError: unable to create new native thread线程数超限、ulimit、线程池无界数组超限java.lang.OutOfMemoryError: Requested array size exceeds VM limit一次性申请超大数组操作系统 OOM Killer进程被 killdmesg 有 Out of memory: Kill process系统内存不足容器限制堆外超限容器 OOMKilledK8s Pod 状态 OOMKilledexit code 137容器内存 limit 被超过二、线上 OOM 排查总流程第 1 步确认现象和影响是单个节点还是全部节点是突然 OOM 还是逐渐变差影响面接口超时、服务不可用、部分功能异常有没有近期发布、配置变更、流量突增第 2 步尽量保留现场不要急着重启先保留证据。如果服务已经无法响应尽快执行堆转储jmap / jcmd / Arthas线程栈jstack / jcmd / Arthas备份 GC 日志记录系统内存、CPU、连接数如果必须重启先做完上述操作。第 3 步收集信息系统层free -h、top、dmesg、ulimit -aJVM 层jstat、jmap、jstack、jcmd、Arthas容器层kubectl describe pod、kubectl top pod第 4 步按类型深入排查见下文三、堆内存 OOM错误信息java.lang.OutOfMemoryError: Java heap space不同工具命令查看 GC 和内存使用率jstat: jstat -gcutil {pid} 1000 10jcmd: jcmd {pid} GC.heap_infoArthas: dashboard 或 jvm 或 memory查看堆对象直方图jmap: jmap -histo:live {pid} | head -20jcmd: jcmd {pid} GC.class_histogramArthas: heapdump生成堆转储或 ognl 查看生成堆转储jmap: jmap -dump:formatb,fileheapdump.hprof {pid}jcmd: jcmd {pid} GC.heap_dump heapdump.hprofArthas: heapdump /tmp/heapdump.hprofjstat 输出示例S0 S1 E O M CCS YGC YGCT FGC FGCT GCT0.00 98.75 65.32 99.87 94.12 91.23 1234 15.678 58 45.234 60.9120.00 97.89 70.11 99.91 94.15 91.25 1235 15.690 59 45.890 61.5800.00 96.54 78.45 99.95 94.18 91.27 1236 15.702 60 46.567 62.269判断依据O老年代使用率持续在 99% 以上Full GC 后不下降。FGCFull GC 次数持续增长。FGCTFull GC 总耗时持续增长且每次 FGC 后 O 依然很高。深入分析查看实例数最多的类jmap: jmap -histo:live {pid} | head -20jcmd: jcmd {pid} GC.class_histogramArthas: 使用 heapdump 生成后 MAT 分析输出示例num #instances #bytes class name1: 5234567 419234560 [B2: 2345678 187654320 com.example.cache.LocalCacheEntry3: 1890123 151209840 java.util.HashMap$Node如果 [Bbyte 数组或某个业务类实例数异常多且持续增长就是重点嫌疑。生成堆转储jmap: jmap -dump:formatb,fileheapdump.hprof {pid}jcmd: jcmd {pid} GC.heap_dump heapdump.hprofArthas: heapdump /tmp/heapdump.hprof用 MAT 打开先看 Leak Suspects 报告它会直接指出哪个对象占用大量内存且被谁引用。如果不够明确打开 Histogram按 Retained Heap 排序找到最大的几个类。对可疑对象右键 - Path to GC Roots - 排除弱引用/软引用查看引用链。如果引用链是 static Map - 说明静态集合未清理。如果是 ThreadLocalMap - 说明 ThreadLocal 未 remove。如果是 ClassLoader - 说明类加载器泄漏。常见根因静态集合如 static Map、static List只增不减。本地缓存无过期/无容量上限。监听器注册后未注销。ThreadLocal 未 remove。大查询一次性加载全量数据。大文件/大报文读入内存。定位代码根据 GC Roots 引用链找到持有对象的类和方法检查是否在循环或请求中不断添加数据而未清理。四、Metaspace OOM错误信息java.lang.OutOfMemoryError: Metaspace不同工具命令查看 Metaspace 使用情况jstat: jstat -gc {pid} 1000 10jcmd: jcmd {pid} VM.metaspaceArthas: memory 或 jvm查看类加载数量jstat: jstat -class {pid} 1000 10jcmd: jcmd {pid} VM.classloader_statsArthas: classloader追踪类加载来源启动参数加 -XX:TraceClassLoadingArthas: 使用 sc 或 sm 查找类jstat -gc 输出示例S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT0.0 0.0 0.0 0.0 102400.0 8192.0 2048000.0 1856000.0 524288.0 489120.0 65536.0 59200.0 1234 15.678 58 45.234 60.912判断依据MCMetaspace 容量接近上限。MUMetaspace 使用量持续增长Full GC 后不下降。深入分析查看类加载数jstat: jstat -class {pid} 1000 10jcmd: jcmd {pid} VM.classloader_statsArthas: classloader输出示例Loaded Bytes Unloaded Bytes Time245678 489120.0 12 24.0 123.45245890 489350.0 12 24.0 123.56Loaded 持续上涨Unloaded 几乎为 0。正常 Spring Boot 应用约 1.5 万个类若达到 20 万以上说明类加载泄漏。启动参数加 -XX:TraceClassLoading观察日志中是否有大量动态生成的类如EnhancerByCGLIBEnhancerByCGLIBEnhancerByCGLIB、DelegatingClassLoader。查看各 ClassLoader 加载的类数量和内存占用jcmd: jcmd {pid} VM.classloader_statsArthas: classloader -t如果某个 ClassLoader 加载了成千上万个类且未卸载就是根因。常见根因CGLIB/动态代理类被静态缓存导致 ClassLoader 无法回收。热部署/OSGi 场景下旧 ClassLoader 泄漏。反射生成大量 DelegatingClassLoader。脚本引擎如 Groovy、JavaScript不断编译生成新类。定位代码检查动态代理、反射、脚本引擎的使用确认是否有静态缓存持有 Class 或 ClassLoader。五、直接内存 OOM错误信息java.lang.OutOfMemoryError: Direct buffer memory前提启动参数需加 -XX:NativeMemoryTrackingdetail。不同工具命令查看 NMT 摘要jcmd: jcmd {pid} VM.native_memory summaryArthas: memory建立基线和差异jcmd: jcmd {pid} VM.native_memory baselinejcmd: jcmd {pid} VM.native_memory detail.diff生成堆转储查看 DirectByteBufferjmap: jmap -dump:formatb,fileheap.hprof {pid}jcmd: jcmd {pid} GC.heap_dump heap.hprofArthas: heapdump /tmp/heap.hprofNetty 泄漏检测启动参数-Dio.netty.leakDetectionLevelPARANOIDjcmd VM.native_memory summary 输出示例Native Memory Tracking:Total: reserved8192000KB, committed4096000KBJava Heap (reserved4096000KB, committed2048000KB)Class (reserved1048576KB, committed524288KB)Thread (reserved102400KB, committed102400KB)Code (reserved204800KB, committed102400KB)GC (reserved409600KB, committed409600KB)Internal (reserved512000KB, committed512000KB) (malloc512000KB, #8000)Symbol (reserved81920KB, committed81920KB)判断依据Internal 或 Other 类别的 committed 持续增长。堆内存正常但进程物理内存RES持续增长。深入分析建立基线jcmd {pid} VM.native_memory baseline等待一段时间后查看差异jcmd {pid} VM.native_memory detail.diff差异报告中 Internal 或 Other 的 committed 增量最大说明这些区域在不断申请内存但未释放。生成堆转储jmap: jmap -dump:formatb,fileheap.hprof {pid}jcmd: jcmd {pid} GC.heap_dump heap.hprofArthas: heapdump /tmp/heap.hprof用 MAT 搜索 java.nio.DirectByteBuffer 实例查看它们的 GC Root 引用链。如果实例数很多且被静态集合或线程持有就是泄漏点。Netty 应用可开启 -Dio.netty.leakDetectionLevelPARANOID泄漏时会打印创建 ByteBuf 的堆栈。常见根因Netty ByteBuf 手动 retain 后未 release。DirectByteBuffer 被缓存长期持有。MappedByteBuffer 映射文件后未 unmap。堆外缓存如某些序列化/压缩库无上限。定位代码根据 DirectByteBuffer 的引用链找到持有它的对象检查是否在请求中不断分配而未释放。六、线程 OOM错误信息java.lang.OutOfMemoryError: unable to create new native thread不同工具命令查看系统线程限制ulimit -a查看当前线程数ps -T -p {pid} | wc -ljstack {pid} | grep ‘^’ | wc -ljcmd {pid} Thread.print | grep ‘^’ | wc -lArthas: thread查看线程栈jstack: jstack {pid} threaddump.txtjcmd: jcmd {pid} Thread.print threaddump.txtArthas: thread threaddump.txtulimit -a 输出示例max user processes (-u) 1024ps -T -p {pid} | wc -l 输出示例1876判断依据线程数接近 ulimit -u 限制。应用需要创建新线程时失败。深入分析执行 jstack {pid} threaddump.txt统计各状态线程数。查找大量线程卡在同一个方法上的情况比如 ThreadPoolExecutor 的队列堆积。检查是否有线程池核心线程数设置过大或者每次请求都 new Thread()。常见根因线程池无界Executors.newCachedThreadPool() 或 newFixedThreadPool 队列无界。每次请求手动创建线程未复用。ulimit -u 设置过低。容器内线程数限制。定位代码检查线程池配置确保使用有界队列和合理的最大线程数避免手动创建线程。七、GC overhead limit exceeded错误信息java.lang.OutOfMemoryError: GC overhead limit exceeded不同工具命令查看 GC 统计jstat: jstat -gcutil {pid} 1000 10jcmd: jcmd {pid} GC.heap_infoArthas: dashboard 或 jvm查看堆对象直方图jmap: jmap -histo:live {pid} | head -20jcmd: jcmd {pid} GC.class_histogramArthas: heapdump 后 MAT 分析jstat -gcutil 输出示例S0 S1 E O M CCS YGC YGCT FGC FGCT GCT0.00 0.00 99.99 99.99 95.00 93.00 5678 123.456 123 456.789 580.245判断依据O 接近 100%FGC 极高FGCT 极大。GC 花费大量时间但回收效果极差。深入分析这通常不是一种独立的泄漏而是堆内存泄漏或堆太小的表现。按堆内存 OOM 的流程分析jmap -histo:live 找大对象MAT 分析引用链。如果堆设置太小调整 -Xmx 后观察是否缓解。常见根因堆内存泄漏同堆 OOM。堆大小设置过小频繁 Full GC。大量临时对象导致 GC 压力大。定位代码同堆内存 OOM。八、系统/容器 OOMKilled错误信息系统dmesg 中出现 Out of memory: Kill process容器K8s Pod 状态 OOMKilledexit code 137不同工具命令系统 OOMdmesg | grep -i “out of memory”grep -i “out of memory” /var/log/messages容器 OOMkubectl describe podkubectl get pod -o yaml | grep -A 5 resourceskubectl top poddmesg 输出示例Out of memory: Kill process 12345 (java) score 800 or sacrifice childKilled process 12345 (java) total-vm:8192000kB, anon-rss:4096000kB判断依据进程被系统杀死不是 JVM 抛 OOM。容器内存 limit 被超过。深入分析检查容器内存 limitkubectl get pod -o yaml 查看 resources.limits.memory。检查 JVM 是否识别容器内存-XX:UseContainerSupport默认开启以及 -Xmx 是否超过 limit 的 70%。检查堆外内存NMT 看直接内存、Metaspace、线程栈等总和是否超过 limit。jcmd: jcmd {pid} VM.native_memory summaryArthas: memory检查系统整体内存free -h、top 看是否有其他进程占用。常见根因JVM 堆 元空间 直接内存 线程栈 容器 limit。多个容器共享节点节点内存不足。-Xmx 设置过大未给堆外留空间。定位代码调整容器 limit 或 JVM 内存参数确保总内存不超过限制。九、排查顺序总结步骤操作工具与命令1看错误信息 / Pod 状态日志、kubectl describe pod2看堆和 Metaspace 趋势jstat -gcutil {pid} 1000jcmd {pid} GC.heap_infoArthas dashboard3看类加载是否异常jstat -class {pid}jcmd {pid} VM.classloader_statsArthas classloader4看堆内对象分布jmap -histo:live {pid}jcmd {pid} GC.class_histogramArthas heapdumpMAT5看线程状态和数量jstack {pid}jcmd {pid} Thread.printArthas thread6看堆外内存分布jcmd {pid} VM.native_memory summaryArthas memory7深度分析堆内泄漏jmap -dump MATjcmd {pid} GC.heap_dump MATArthas heapdump MAT8分析 DirectByteBuffer 引用jmap -dump MATjcmd {pid} GC.heap_dump MATArthas heapdump MAT9确认系统/容器 OOMKilleddmesgkubectl describe pod关键原则先定类型再看趋势最后做深度分析。每一类都按上述流程走到底才能从现象追到代码根因。十、常用工具速查工具命令用途jstatjstat -gcutil {pid} 1000实时查看 GC 和内存区域使用率jstatjstat -class {pid} 1000查看类加载数量jmapjmap -histo:live {pid}查看堆内对象实例数和大小jmapjmap -dump:formatb,fileheap.hprof {pid}生成堆转储jstackjstack {pid}查看线程栈jcmdjcmd {pid} VM.native_memory summary查看 JVM 本地内存分布jcmdjcmd {pid} VM.classloader_stats查看类加载器统计jcmdjcmd {pid} GC.heap_info查看堆内存信息jcmdjcmd {pid} GC.class_histogram查看堆对象直方图jcmdjcmd {pid} GC.heap_dump heap.hprof生成堆转储jcmdjcmd {pid} Thread.print打印线程栈jcmdjcmd {pid} VM.metaspace查看 Metaspace 信息Arthasdashboard实时监控面板Arthasjvm查看 JVM 信息Arthasmemory查看内存信息Arthasthread查看线程信息Arthasheapdump /tmp/heap.hprof生成堆转储Arthasclassloader查看类加载器统计Arthassc/sm查找类和方法MAT打开 hprof 文件分析堆转储找泄漏根因dmesgdmesg | grep -i “out of memory”查看系统 OOM Killer 日志kubectlkubectl describe pod查看容器 OOMKilled 事件
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进