跳到主要内容

OOM 排查

问题

Java 中有哪些常见的 OOM 类型?如何排查和解决 OutOfMemoryError?

答案

OOM 类型全览

OOM 错误信息发生区域常见原因
Java heap space对象过多、内存泄漏、堆太小
GC overhead limit exceededGC 占 98% 时间但只回收 2% 内存
Metaspace元空间动态生成类过多(CGLIB、反射)
Direct buffer memory堆外内存NIO DirectByteBuffer 未释放
unable to create new native threadOS线程数超过系统限制
Requested array size exceeds VM limit申请了超大数组
Out of swap spaceOS物理内存和交换空间都耗尽
Kill process or sacrifice childOSLinux OOM Killer 杀死进程

排查总流程

配置自动 Heap Dump

生产环境必须配置,OOM 时自动导出堆转储文件:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump.hprof

1. Java heap space

最常见的 OOM,堆空间不足以分配新对象。

常见原因:

HeapOOMExamples.java
// 原因1:大量对象无法回收(内存泄漏)
public class MemoryLeak {
// 静态集合不断增长,对象无法被 GC
private static List<byte[]> cache = new ArrayList<>();

public void addData() {
// 每次调用都往静态集合中添加数据,从不清理
cache.add(new byte[1024 * 1024]); // 1MB
}
}

// 原因2:一次性加载大量数据
public void loadAllData() {
// 一次性查询数据库全表,数据量巨大
List<Record> allRecords = db.selectAll(); // 可能几百万条
}

// 原因3:大对象创建
public void createHugeArray() {
byte[] huge = new byte[Integer.MAX_VALUE]; // 请求 2GB 连续空间
}

排查步骤:

# 1. 查看 GC 日志,确认老年代使用情况
jstat -gcutil <pid> 1000

# 2. 导出 Heap Dump(如果没有自动导出)
jmap -dump:live,format=b,file=heap.hprof <pid>

# 3. 用 MAT 分析
# 重点关注:
# - Leak Suspects Report(泄漏嫌疑报告)
# - Dominator Tree(支配树,按占用内存排序)
# - Top Consumers(内存消耗最大的对象)

解决方案:

  1. 修复内存泄漏(释放不再需要的引用)
  2. 增大堆内存(-Xmx
  3. 分批处理大数据量(分页查询)
  4. 使用流式处理代替全量加载

2. GC overhead limit exceeded

JVM 检测到 GC 花费 98% 以上的时间,但只回收了 2% 不到的堆内存,认为 GC 已经无效。

本质上也是堆内存不足,只是 JVM 提前报警:

# 如果想禁用这个检测(不推荐,只是推迟问题)
-XX:-UseGCOverheadLimit

# 正确做法:和 heap space OOM 一样排查
# 增大堆或修复内存泄漏

3. Metaspace OOM

元空间存储类的元信息,类加载过多时触发。

常见原因:

MetaspaceOOM.java
// 原因1:动态代理/CGLIB 生成大量类
// Spring AOP 默认使用 CGLIB,每个代理生成一个新类
@Aspect
public class LogAspect {
// 大量 Bean 都被代理时,元空间可能不足
}

// 原因2:热部署/类加载器泄漏
// Tomcat 热部署时旧的 WebAppClassLoader 未被回收
// 导致旧类无法卸载,元空间不断增长

// 原因3:大量 JSP 编译(每个 JSP 编译为一个 Servlet 类)
// 原因4:大量使用 Lambda 表达式(每个 Lambda 生成一个匿名类)

排查步骤:

# 查看元空间使用情况
jstat -gcmetacapacity <pid>

# 查看加载的类数量
jcmd <pid> VM.classloader_stats

# 或通过 JMX
ManagementFactory.getClassLoadingMXBean().getLoadedClassCount()

解决方案:

# 增大元空间
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# 同时检查并修复类加载器泄漏
# Tomcat 热部署问题:升级 Tomcat 或使用 JRebel
# 过多动态代理:检查 AOP 粒度

4. Direct buffer memory

NIO 的 ByteBuffer.allocateDirect() 分配的堆外内存超过限制。

DirectBufferOOM.java
// 堆外内存没有被手动释放
public void leakDirectBuffer() {
while (true) {
// 分配堆外内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// buffer 变成垃圾后,堆外内存依赖 Cleaner(虚引用)回收
// 如果 GC 不及时,堆外内存可能累积
}
}

解决方案:

# 增大直接内存限制
-XX:MaxDirectMemorySize=512m

# 使用 Netty 时,开启内存泄漏检测
-Dio.netty.leakDetection.level=PARANOID

代码层面:使用完 DirectByteBuffer 后,主动调用 ((DirectBuffer) buffer).cleaner().clean() 释放,或使用 Netty 的引用计数管理。

5. unable to create new native thread

无法创建更多线程。

# Linux 查看系统线程限制
ulimit -u # 单用户最大进程/线程数
cat /proc/sys/kernel/threads-max # 系统最大线程数

# 查看当前进程线程数
ls /proc/<pid>/task | wc -l
# 或
jstack <pid> | grep 'tid' | wc -l

常见原因与解决:

原因解决方案
线程池配置不合理(核心线程数太大)合理设置线程池大小
线程泄漏(创建了不销毁)使用线程池,避免 new Thread()
系统 ulimit 限制太小ulimit -u 65535
每个线程栈占用过大减小 -Xss(如 256k)
堆过大导致留给线程栈的内存不足适当减小 -Xmx
线程数计算公式

可创建线程数 ≈ (系统可用内存 - 堆内存 - 元空间 - 其他) / 线程栈大小

如果堆设为 4GB,每个线程栈 1MB,那剩余内存决定了最多能创建多少线程。

常见内存泄漏场景

场景原因解决方案
静态集合静态 List/Map 只增不删使用缓存框架(Caffeine),设置上限和过期
未关闭的资源Connection/Stream/Cursor 未关闭try-with-resources
监听器/回调注册后未注销及时 removeListener
ThreadLocal线程池中 ThreadLocal 未 remove用完在 finally 中 remove()
内部类持有外部类引用非静态内部类隐式持有外部类引用使用静态内部类 + WeakReference
HashMap 的 key 未正确实现 hashCode/equals同一对象每次 put 都创建新 entry正确实现 hashCode/equals
缓存未设上限本地缓存(HashMap)无限增长使用 Caffeine/Guava Cache
CommonLeaks.java
// ❌ ThreadLocal 泄漏
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

public void setUser(User user) {
currentUser.set(user);
}
// 线程池中线程被复用,ThreadLocal 的值不会被清理
}

// ✅ 正确用法
public void handleRequest() {
try {
UserContext.setUser(getUser());
// 业务处理
} finally {
UserContext.currentUser.remove(); // 必须清理
}
}

MAT 分析 Heap Dump

Eclipse MAT 是分析 Heap Dump 最常用的工具:

核心功能:

功能说明
Leak Suspects自动分析泄漏嫌疑对象,生成报告
Dominator Tree按 Retained Heap 排序,找出占用内存最大的对象
Histogram按类统计对象数量和大小
GC Roots查看对象的 GC Root 引用链,定位为什么不能被回收
OQL类似 SQL 的查询语言,灵活查找对象

关键概念:

概念含义
Shallow Heap对象自身占用的内存(不含引用的对象)
Retained Heap对象被回收后能释放的总内存(含只被它引用的对象)
Dominator如果回收对象 A 就能回收对象 B,则 A 是 B 的支配者

分析流程:

1. 打开 Heap Dump → 查看 Leak Suspects Report
2. 查看 Dominator Tree → 找到 Retained Heap 最大的对象
3. 右键 → "Path to GC Roots" → "exclude weak/soft references"
4. 查看引用链 → 定位到业务代码
5. 修复代码 → 重新压测验证

常见面试问题

Q1: 线上出现 OOM 怎么排查?

答案

  1. 确认 OOM 类型:查看错误日志中的具体错误信息
  2. 获取 Heap Dump:如果配置了 -XX:+HeapDumpOnOutOfMemoryError 直接取;否则用 jmap 导出
  3. MAT 分析
    • 查看 Leak Suspects 报告
    • 查看 Dominator Tree 找大对象
    • 通过 GC Roots 引用链定位代码
  4. 结合 GC 日志:确认 GC 频率、老年代增长趋势
  5. 修复并验证:修改代码后压测验证

Q2: Java heap space 和 GC overhead limit exceeded 有什么区别?

答案

两者都是堆内存不足,但触发时机不同:

  • Java heap space:分配对象时,堆空间不足以容纳新对象
  • GC overhead limit exceeded:GC 花费 98% 以上的时间,但只回收了不到 2% 的内存

后者是 JVM 的提前预警机制,意味着 GC 已经无效,很快就会出现 heap space OOM。两者的排查和解决方法相同。

Q3: 如何避免内存泄漏?

答案

  1. 资源关闭:使用 try-with-resources 关闭 Connection/Stream/Socket
  2. ThreadLocal:使用后在 finally 中 remove()
  3. 缓存控制:使用 Caffeine/Guava Cache 设置最大容量和过期时间,不要用 HashMap 做缓存
  4. 监听器管理:注册的监听器在不需要时注销
  5. 静态集合:避免静态 List/Map 无限增长
  6. 内部类:使用静态内部类 + WeakReference 代替非静态内部类
  7. 定期检查:代码审查 + 定期分析 Heap Dump

Q4: 元空间 OOM 怎么排查?

答案

  1. 通过 jstat -gcmetacapacity <pid> 查看元空间使用情况
  2. 通过 jcmd <pid> VM.classloader_stats 查看类加载器信息
  3. 常见原因:
    • 动态代理/CGLIB 生成过多类:Spring AOP 代理、MyBatis Mapper
    • 热部署导致类加载器泄漏:Tomcat 重载 WAR 包
    • 大量 Lambda 表达式:每个 Lambda 生成一个匿名内部类
  4. 解决:增大 -XX:MaxMetaspaceSize,同时修复根因

Q5: Shallow Heap 和 Retained Heap 有什么区别?

答案

  • Shallow Heap:对象自身占用的内存大小(对象头 + 实例变量),不包含引用的其他对象
  • Retained Heap:如果该对象被 GC 回收,总共能释放多少内存(包含只被该对象引用的所有对象)

Retained Heap 更重要,它反映了一个对象"真正占用"的内存。MAT 中按 Retained Heap 排序可以快速找到内存大户。

Q6: 如何监控 JVM 内存使用?

答案

工具用途
jstat -gcutil <pid> 1000命令行,实时查看 GC 统计
VisualVM / JConsoleGUI,实时监控堆、线程、类加载
Arthas(阿里)在线诊断,dashboard 命令查看实时数据
Prometheus + Grafana生产环境监控,配合 Micrometer/JMX Exporter
Spring Boot Actuator/actuator/metrics 暴露 JVM 指标

生产环境推荐 Prometheus + Grafana 方案,设置告警规则(如堆使用率 > 80%、Full GC 次数 > 0)。

相关链接