OOM(Out Of Memory)是生产环境最严重的故障之一,必须快速响应。以下是系统化的排查方案,包含应急处理→根因定位→解决方案全流程:
一、OOM分类与特征速查
| OOM类型 | 错误信息特征 | 常见原因 |
|---|---|---|
| 堆内存溢出 | java.lang.OutOfMemoryError: Java heap space | 内存泄漏、大对象、缓存失控 |
| 元空间溢出 | java.lang.OutOfMemoryError: Metaspace | 动态类加载过多(CGLib等) |
| 直接内存溢出 | java.lang.OutOfMemoryError: Direct buffer memory | NIO未释放、Netty配置不当 |
| 栈内存溢出 | java.lang.StackOverflowError | 递归调用过深、循环依赖 |
| 线程数溢出 | java.lang.OutOfMemoryError: unable to create new native thread | 线程泄漏、ulimit限制 |
二、黄金30分钟应急响应流程
步骤1:保留现场(关键!)
bashCopy Code
# 1. 立即保存堆转储(Heap Dump) jmap -dump:format=b,file=/tmp/heap_oom.hprof <pid> # 2. 抓取线程快照 jstack -l <pid> > /tmp/jstack_oom.txt # 3. 保存GC日志(如果开启) cp $GC_LOG_PATH /tmp/gc_oom.log # 4. 容器环境保存内存快照(如K8s) kubectl debug <pod> --copy-to=<debug-pod> -- /bin/sh
步骤2:快速恢复服务
bashCopy Code
# 1. 临时扩容内存(堆/元空间) java -Xmx8g -XX:MaxMetaspaceSize=512m ... # 2. 安全重启(容器优先重启单实例) kubectl rollout restart deploy/<app> --partition=1 # 灰度重启
步骤3:流量降级保护
- 熔断非核心功能(如关闭报表生成)
- 限流降级(Sentinel/Sentinel配置QPS阈值)
三、根因定位三板斧
斧1:堆内存分析(MAT/JVisualVM)
操作流程:
- 用Eclipse MAT加载
heap_oom.hprof - 定位最大内存对象:
javaCopy Code
// 查看支配树(Dominator Tree) -> 按 Retained Heap 降序排序 -> 定位占用 >30% 的对象 - 检查可疑对象引用链:

典型内存泄漏模式:
- 静态集合膨胀:
static Map持续加入数据无清理 - 线程局部变量未释放:ThreadLocal 使用后未 remove
- 资源未关闭:JDBC Connection、文件流未 close
斧2:元空间分析(JCMD + Arthas)
bashCopy Code
# 1. 查看类加载情况 jcmd <pid> VM.classloader_stats | grep 'Loaded' # 2. 用Arthas跟踪动态类生成 arthas> watch org.springframework.cglib.proxy.Enhancer create
元空间泄漏特征:
- Groovy/Spring CGLib代理:每秒新增多个
$$EnhancerBySpringCGLIB$$类 - OSGi热部署:频繁加载/卸载类
斧3:直接内存分析(Netty场景)
javaCopy Code
// 启用Direct Memory监控 -Dio.netty.maxDirectMemory // 显示配置大小 -Dio.netty.leakDetectionLevel=PARANOID // 启用泄漏检测
分析工具:
bashCopy Code
# 查看Direct Memory占用 jcmd <pid> VM.native_memory summary | grep 'Internal' # 使用Google的jemalloc分析内存分布 MALLOC_CONF=prof:true java ...
四、高频OOM场景解决方案
场景1:堆内存泄漏(静态Map引起)
修复方案:
javaCopy Code
// 原错误代码:静态Map无限增长 private static Map<String, Object> CACHE = new HashMap<>(); // 修复方案1:使用WeakHashMap(GC自动回收) private static Map<String, Object> CACHE = new WeakHashMap<>(); // 修复方案2:添加LRU淘汰策略 private static Map<String, Object> CACHE = Collections.synchronizedMap( new LinkedHashMap<>(1000, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 1000; } } );
场景2:元空间溢出(Spring CGLib代理过多)
修复方案:
javaCopy Code
// 在启动参数限制代理类缓存 -XX:+UseLambdaFormInvokers // JDK8+优化invoker -Dspring.objenesis.ignore=true // 禁用Spring Objenesis缓存 // 代码层面:避免滥用@Scope("prototype") @Bean @Scope(proxyMode = ScopedProxyMode.NO) // 禁用代理 public MyBean myBean() { ... }
场景3:Netty直接内存泄漏
修复方案:
javaCopy Code
// 确保ByteBuf引用计数归零 try { ByteBuf buf = ...; // 业务逻辑 } finally { ReferenceCountUtil.release(buf); // 强制释放 } // 启用Netty内存检测(生产环境谨慎) ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);
五、防御性编程与监控加固
1. 代码规约
javaCopy Code
// 强制规则1:所有资源实现AutoCloseable try (Connection conn = dataSource.getConnection()) { ... } // 自动关闭 // 强制规则2:ThreadLocal必须remove ThreadLocal<User> userHolder = ...; try { userHolder.set(user); ... } finally { userHolder.remove(); }
2. 监控基线配置
yamlCopy Code
# Prometheus监控规则 groups: - name: JVM OOM预警 rules: - alert: HeapMemoryOver85% expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85 for: 5m - alert: MetaspaceOver90% expr: jvm_memory_used_bytes{area="nonheap", id="Metaspace"} / jvm_memory_max_bytes{area="nonheap", id="Metaspace"} > 0.9
3. 压测验证
bashCopy Code
# 使用JMeter模拟OOM场景 jmeter -Jthreads=500 -Jrampup=60 -n -t oom_test.jmx
六、终极武器:堆转储分析技巧
-
MAT过滤技巧:
sqlCopy Code
// 查找疑似泄漏点(按类分组) SELECT * FROM java.util.HashMap WHERE size() > 1000 // 查找大数组 SELECT * FROM byte[] WHERE length > 1048576 // >1MB -
对比分析:
- 在压测前后分别dump堆内存
- 使用MAT的「Compare Basket」功能定位增长对象
核心原则:
- 优先保存现场再重启
- 堆转储文件 > 日志 > 猜测
- 根治方案 = 代码修复 + 资源约束 + 监控告警
通过以上流程,可将OOM平均解决时间(MTTR)从小时级降至分钟级,并有效预防复发。
380



被折叠的 条评论
为什么被折叠?



