OOM 定位与排查
问题
线上服务抛出 java.lang.OutOfMemoryError,如何定位根因?
答案
OOM 类型速查
| OOM 类型 | 含义 | 常见原因 |
|---|---|---|
Java heap space | 堆内存不足 | 大对象、内存泄漏 |
GC overhead limit exceeded | GC 耗时超 98% 回收不到 2% | 内存泄漏 |
Metaspace | 元空间不足 | 动态代理/CGLIB 创建过多类 |
Direct buffer memory | 直接内存不足 | NIO ByteBuffer 未释放 |
unable to create new native thread | 线程数超限 | 线程池失控 |
Requested array size exceeds VM limit | 数组过大 | 不合理的数组分配 |
排查流程
JVM 参数配置(关键)
生产环境必备 JVM 参数
# OOM 时自动 dump 堆内存(最重要!)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump/
# OOM 后自动重启(可选)
-XX:OnOutOfMemoryError="kill -9 %p"
# GC 日志
-Xlog:gc*:file=/data/logs/gc/gc.log:time,level,tags:filecount=10,filesize=100m
生产必加
-XX:+HeapDumpOnOutOfMemoryError 是排查 OOM 最关键的参数。没有堆转储文件,排查难度大幅增加。
使用 MAT 分析堆转储
- Leak Suspects Report:自动分析疑似泄漏点
- Dominator Tree:按对象大小排序,找到最大对象
- Histogram:按类统计实例数和内存占用
- GC Roots → Path to GC Roots:查看大对象的引用链,定位是谁持有引用
MAT 分析思路
Dominator Tree 发现:
ArrayList → 占堆 80%
→ 包含 500 万个 OrderDTO 对象
→ 被 OrderService.cache 字段引用
→ 是 static Map,只 put 不 remove → 内存泄漏!
常见 OOM 场景与修复
场景 1:一次查询全量数据
❌ 错误:一次加载全表数据
List<Order> orders = orderMapper.selectAll(); // 500 万条 → OOM
✅ 修复:分页查询 + 流式处理
int page = 0, size = 500;
List<Order> batch;
do {
batch = orderMapper.selectPage(page++, size);
processBatch(batch);
} while (batch.size() == size);
场景 2:本地缓存无上限
❌ 错误:HashMap 无限增长
private static final Map<String, Object> CACHE = new HashMap<>();
public Object get(String key) {
return CACHE.computeIfAbsent(key, k -> loadFromDB(k)); // 只增不减
}
✅ 修复:使用有限缓存
// Caffeine 带大小限制 + 过期时间
private final Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
场景 3:Metaspace OOM
原因:动态生成大量类
// 大量 CGLIB / Javassist 动态代理 → 类越来越多
// Spring AOP 对非 final 类默认用 CGLIB
修复:增大元空间 + 排查动态类生成
-XX:MaxMetaspaceSize=512m
# 用 Arthas 查看类加载情况
classloader -l
常见面试问题
Q1: 线上 OOM 你怎么排查?
答案:
- 确认 OOM 类型(看异常信息)
- 拿到 heapdump 文件(
-XX:+HeapDumpOnOutOfMemoryError) - MAT 打开,看 Leak Suspects 和 Dominator Tree
- 找到占内存最大的对象 → 查看 GC Roots 引用链 → 定位代码
Q2: heap space OOM 和 GC overhead 有什么区别?
答案:
heap space:分配对象时堆内存不够GC overhead limit exceeded:GC 花了 98%+ 的时间但只回收了不到 2% 的堆内存- 后者更多暗示内存泄漏——有大量对象无法被回收
Q3: 如何在不重启的情况下获取堆转储?
答案:
# jmap 手动导出(会触发 Full GC,慎用)
jmap -dump:format=b,file=/tmp/heapdump.hprof <pid>
# Arthas(更推荐)
heapdump /tmp/heapdump.hprof
Q4: unable to create new native thread 怎么处理?
答案:
- 通常是线程数超过系统限制(
ulimit -u)或内存不足 - 排查:
jstack看线程数 →ps -eLf | grep java | wc -l - 原因:线程池不复用(每次
new Thread)、线程池参数不合理 - 详见 线程池