跳到主要内容

OOM 定位与排查

问题

线上服务抛出 java.lang.OutOfMemoryError,如何定位根因?

答案

OOM 类型速查

OOM 类型含义常见原因
Java heap space堆内存不足大对象、内存泄漏
GC overhead limit exceededGC 耗时超 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 分析堆转储

  1. Leak Suspects Report:自动分析疑似泄漏点
  2. Dominator Tree:按对象大小排序,找到最大对象
  3. Histogram:按类统计实例数和内存占用
  4. 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 你怎么排查?

答案

  1. 确认 OOM 类型(看异常信息)
  2. 拿到 heapdump 文件(-XX:+HeapDumpOnOutOfMemoryError
  3. MAT 打开,看 Leak Suspects 和 Dominator Tree
  4. 找到占内存最大的对象 → 查看 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)、线程池参数不合理
  • 详见 线程池

相关链接