线上出现的OOM是如何排查的?
1 个回答
世间万物,唯有过往伤人最深。冰心月# 1) 先判定是哪种 OOM(非常关键) 看报错信息/监控/容器事件,区分类型,不同解法完全不一样: * **Java heap** * `java.lang.OutOfMemoryError: Java heap space` * `GC overhead limit exceeded`(一直 GC 但回收极少) * **Direct/Off-heap** * `java.lang.OutOfMemoryError: Direct buffer memory`(NIO/Netty/ByteBuffer) * 原生内存耗尽(JNA、压缩、TLS、线程栈等) * **Metaspace** * `java.lang.OutOfMemoryError: Metaspace`(类加载泄漏/频繁重载) * **系统/容器把进程杀了** * K8s:`OOMKilled`;Linux:`dmesg` 里 `Out of memory: Kill process` * JVM 日志未必有 OOM 栈 > **判定方法**:应用日志、K8s 事件 (`kubectl describe pod`)、系统日志 (`dmesg`)、JVM 日志(GC/错误日志)。 # 2) 快速止血(把服务活下来) * **扩容/降流/限并发**:临时缩小线程池或限流;必要时扩副本。 * **增配但留余量**:容器 `memoryLimit` ↑,同时 **Xmx 要明显小于 limit**(给 off-heap/线程/代码缓存留 30% 余量)。 * **降级**:关闭大对象功能(一次性加载、导出、聚合)、降低批量大小。 * **打开取证开关**(见下一步),重启服务。 # 3) 取证与快速体检(上线就该常备) **JVM 启动参数(长期建议)** * `-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump`(自动导出 heap dump) * `-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags`(JDK9+;JDK8 用 `-Xloggc`) * (可选)`-XX:NativeMemoryTracking=summary`(开销小)或 `detail` **线上排查常用命令** * 线程/堆: * `jcmd <pid> GC.heap_info`、`jcmd <pid> GC.class_histogram` * `jmap -histo:live <pid> | head -50`(大对象分布) * `jmap -dump:live,format=b,file=heap.hprof <pid>` * `jstack -l <pid>`(看看是否卡某些线程/ThreadLocal 泄漏) * 原生内存: * `jcmd <pid> VM.native_memory summary`(NMT;看 Direct / Internal / Thread 等占用) * 容器/系统: * `kubectl top pod` / `describe pod`(`OOMKilled`) * `ps -o pid,rss,command -p <pid>`、`pmap -x <pid>`、`dmesg | tail` # 4) 对症下药(不同 OOM 的根因与修复) ## A. Heap OOM / GC Overhead **症状**:堆占用持续上升或 Full GC 频繁。 **定位**:用 dump 在 MAT/VisualVM 看 **Dominator Tree**、GC Roots 路径,找最大保留集。 **常见根因 & 修复** * **无限制缓存/Map**(没有 TTL/最大容量)→ 上 Caffeine/Guava **size/weight + TTL**;监控命中率与大小 * **聚合/拼装大对象**(一次性 `toList()`、大 `StringBuilder`、无分页)→ 分页/分块处理,流式读写 * **ThreadLocal 泄漏**(未 `remove`)→ try/finally 清理;线程池复用时尤需注意 * **反序列化/JSON 组装大对象** → 换流式解析(Jackson streaming)、限制字段 * **序列化队列堆积**(MQ/队列消费不过来)→ 限流/背压,调小批量 **调参兜底**: * 合理设置 **Xms=Xmx**(避免频繁扩容),用 **G1/GenZ**; * 检查 `-XX:MaxRAMPercentage`(容器内)与 Limit 的匹配,确保 **Xmx < Limit × 0.7** 左右。 ## B. Direct buffer / Off-heap OOM **症状**:报 `Direct buffer memory` 或 RSS 高而堆不高。 **定位**:NMT(`VM.native_memory summary`)、应用指标(Netty PooledArena)、`-XX:MaxDirectMemorySize`。 **根因 & 修复** * **未释放 ByteBuffer/Netty ByteBuf** → 确保 `release()`;用 try-with-resources 包装;开启 Netty 泄漏检测 `-Dio.netty.leakDetectionLevel=paranoid`(只在测试/预发) * **无上限的直接内存** → 配置 `-XX:MaxDirectMemorySize=256m~1g`;Netty 配对象池且限制最大并发 * **大响应/压缩/SSL 原生内存** → 减小批量、分块;确认本地/Native 库版本 ## C. Metaspace OOM **症状**:`OutOfMemoryError: Metaspace`。 **根因**:**类加载器泄漏**(反复热加载/动态代理生成类、URLClassLoader 未关闭)。 **修复**: * 修正热加载逻辑;减少频繁生成新 Class; * 调整 `-XX:MaxMetaspaceSize` 但重点是修复泄漏; * `jcmd <pid> VM.classloaders`、`GC.class_stats` 辅助判断。 ## D. “unable to create new native thread” **症状**:线程数爆表或系统 `ulimit -u` 限制。 **修复**: * **控线程池并发**、合并池;合理 `queue` 与拒绝策略; * 调整容器/系统线程数限制; * 用 `jstack`/`top -H` 找线程源头(哪个池在疯狂建线程)。 ## E. 容器 OOMKilled(非 JVM 报 OOM) **症状**:Pod 被杀,JVM没栈。 **修复**: * JVM 堆(Xmx)+ 原生内存之和 **必须显著小于** K8s limit; * JDK11+ 使用 `-XX:MaxRAMPercentage`(如 50)配合 limit; * 预留线程栈/Direct/Metaspace/CodeCache 等空间(通常 ≥ 30% 预留)。 # 5) 防复发(工程化治理) * **监控告警**:堆使用率、Young/Old GC 次数与时间、Full GC、RSS、Direct 内存、线程数、类加载数、对象分配速率。 * **容量/压测**:关键接口做 3× 峰值压测,观察 P95/P99 与 RSS;设置**自动化内存回归测试**。 * **限流与背压**:线程池有界队列 + 合理拒绝策略;对外部依赖设置超时与舱壁。 * **数据/批量**:所有批处理/导出/聚合都**限制批大小**并流式处理。 * **开关与熔断**:配置化关闭大对象功能;遇压自动降级。 ## 面试可直接说的“总结回答” > 我会先区分 OOM 类型:是 Heap、Direct、Metaspace 还是被容器 OOMKilled。线上常开 `HeapDumpOnOutOfMemoryError`、GC 日志和 NMT。止血上先限流/降并发,必要时扩容并预留 headroom。随后导出 heap dump 用 MAT 看 Dominator Tree 和 GC Roots;若是 off-heap,则用 `jcmd VM.native_memory summary` 或 Netty 指标定位未释放的 ByteBuf 或直接内存上限没配。容器侧核对 K8s limit 与 Xmx 的关系,确保 Xmx 只占 60–70%。修复一般是:给缓存加 TTL/上限、所有大查询改分批/流式、清理 ThreadLocal、限制批量与直接内存、收敛线程池并发。最后通过压测与监控验收,并把这些开关参数化,防止再次发生。发布于 2025-08-12 04:26 •IP属地 四川举报评论收藏
公众号进入网站体验更佳

