一、首先,理解 OOM 的类型
OOM 错误并非只有一种,明确错误类型是排查的第一步。常见的 OOM 类型及原因:
-
java.lang.OutOfMemoryError: Java heap space- 原因:堆内存不足。这是最常见的 OOM,表示创建的新对象无法在堆中分配足够空间。
-
java.lang.OutOfMemoryError: Metaspace(Java 8+) /PermGen space(Java 7-)- 原因:元空间(或永久代)不足。元空间用于存储类的元数据(如类名、方法信息、字节码等)。通常由动态类生成(如大量使用 CGLib、反射、JSP)或部署了大量应用导致。
-
java.lang.OutOfMemoryError: Unable to create new native thread- 原因 :创建的线程数超过系统限制。可能因为应用创建了太多线程,或者系统(如 Linux)给每个进程的线程数限制(
ulimit -u)过低。
- 原因 :创建的线程数超过系统限制。可能因为应用创建了太多线程,或者系统(如 Linux)给每个进程的线程数限制(
-
java.lang.OutOfMemoryError: GC overhead limit exceeded- 原因:GC 开销过大。JVM 花费了超过 98% 的时间进行垃圾回收,但只回收了不到 2% 的堆空间,意味着 GC 在做无用功,应用基本已无法推进。
-
java.lang.OutOfMemoryError: Requested array size exceeds VM limit- 原因 :尝试分配一个大于 JVM 允许的最大数组大小(通常是
Integer.MAX_VALUE - 2)。
- 原因 :尝试分配一个大于 JVM 允许的最大数组大小(通常是
二、排查工具一览
| 工具 | 用途 | 特点 |
|---|---|---|
| JVM 内置参数 | 记录关键信息 | 无需额外工具,必须在启动前配置 |
jps |
查看 Java 进程号 | 基础命令行工具 |
jstat |
监控 GC 状态 | 轻量级,实时查看 GC 和内存分区使用情况 |
jmap |
生成堆转储文件 | 获取堆内存快照,用于离线分析 |
jstack |
生成线程转储文件 | 分析线程状态,排查死锁或线程过多问题 |
| VisualVM | 图形化监控和分析 | JDK 自带,功能全面,直观易用 |
| Eclipse MAT | 分析堆转储文件 | 分析 Heap Dump 的首选工具,强大且高效 |
| Arthas | 在线诊断工具 | 阿里开源,无需重启项目,动态诊断神器 |
三、具体排查步骤(从易到难)
阶段一:初步诊断与信息收集
-
确认 OOM 类型 :仔细阅读错误日志的第一行,确定是哪种
OutOfMemoryError。这直接决定了后续的排查方向。 -
添加 JVM 参数(最重要的步骤) :
在应用启动时添加以下参数,以便在下次出现 OOM 时自动捕获关键信息。
-XX:+HeapDumpOnOutOfMemoryError # 在发生OOM时自动生成堆转储文件(Heap Dump)
-XX:HeapDumpPath=/path/to/dump.hprof # 指定Heap Dump的保存路径
-XX:+PrintGCDetails # 打印详细的GC日志
-Xloggc:/path/to/gc.log # 将GC日志输出到文件
如果已经发生了 OOM 但没有这些参数,请务必加上它们然后重现问题。
-
监控实时状态:
- 使用
jps找到应用的进程 ID (PID)。 - 使用
jstat -gc <pid> 1000(每秒钟一次)来监控堆内存各分区(Eden, Old等)的使用情况和 GC 次数/时间。如果老年代(OGC/UGC)使用率持续居高不下且频繁 Full GC,说明很可能有内存泄漏。
- 使用
阶段二:深度分析 - 针对 Java heap space 和 Metaspace
核心工作:分析 Heap Dump
-
获取 Heap Dump:
- 自动生成 :通过上述
-XX:+HeapDumpOnOutOfMemoryError参数,OOM 时自动生成。 - 手动生成 :使用
jmap命令在任意时间点手动生成。
jmap -dump:live,format=b,file=dump.hprof
- 自动生成 :通过上述
-
使用 MAT 分析 Heap Dump:
- 将
.hprof文件导入 Eclipse Memory Analyzer Tool (MAT)。 - 第一步 :查看 Leak Suspects Report(泄漏嫌疑报告)。MAT 会自动分析并给出可能发生内存泄漏的疑点,这是最快最有效的入门方法。
- 第二步 :使用 Histogram (直方图)功能。查看哪些类的对象数量最多、占用内存最大。重点关注
Shallow Heap和Retained Heap大的类。Shallow Heap: 对象本身占用的内存。Retained Heap: 该对象被回收后,能连带释放的总内存(这是关键指标)。
- 第三步 :对疑似有问题的类,右键选择 Merge Shortest Paths to GC Roots -> exclude all weak/soft references 。这会显示这些对象为什么没有被垃圾回收------即是谁在持有它们的强引用(Strong Reference)。这个引用链的根部通常就是问题的根源(如某个全局的静态集合、未关闭的连接等)。
- 将
-
分析 GC 日志 :
使用 GCeasy 或 GCHisto 等在线/离线工具上传
gc.log文件。它们可以生成可视化报告,帮你判断:- 内存分配速率是否过高?
- GC 效率如何?平均暂停时间多久?
- 是否发生了内存提升(Promotion Failure)或疏散失败(Evacuation Failure)?
对于 Metaspace OOM:
- 使用
jstat -gc <pid>查看MC(Metaspace Capacity) 和MU(Metaspace Usage) 的使用情况。 - 检查是否使用了大量动态代理、反射(如 Spring AOP)、或热部署。
- 可以考虑适当调大元空间大小(但仅是临时方案):
-XX:MaxMetaspaceSize=256m
阶段三:针对其他类型 OOM
-
Unable to create new native thread:- 使用
jstack <pid>导出线程转储,查看线程数量和工作状态。 - 检查代码中是否有线程创建未正确关闭的情况(例如,使用线程池而非无限循环
new Thread())。 - 检查系统级的线程数限制(Linux 下使用
ulimit -u查看)。
- 使用
-
GC overhead limit exceeded:- 排查思路与
Java heap space类似,这通常是内存泄漏的一个极端表现。同样需要分析 Heap Dump 找到无法回收的对象。
- 排查思路与
四、常见原因与修复策略
-
内存泄漏:这是最普遍的根源。
- 场景:静态集合类持有了大量对象引用且未及时清理;未关闭的资源(数据库连接、文件流、网络连接);监听器注册后未注销;内部类持有外部类的引用等。
- 修复 :根据 MAT 找到的引用链,修复代码,在适当的地方释放引用(如调用
remove(),close()方法)。
-
堆内存设置过小:
- 场景 :应用本身确实需要大量内存(如处理大文件、大数据集),但
-Xmx参数设置太小。 - 修复 :根据监控情况,合理调大堆内存参数(
-Xms,-Xmx)。
- 场景 :应用本身确实需要大量内存(如处理大文件、大数据集),但
-
代码问题:
- 场景 :循环中创建大量对象;使用了不合理的数据结构(用
HashMap存储少量数据,但初始化容量initialCapacity巨大)。 - 修复:优化代码逻辑,避免不必要的对象创建,重用对象(使用对象池)。
- 场景 :循环中创建大量对象;使用了不合理的数据结构(用
配置 -XX:+HeapDumpOnOutOfMemoryError 是重中之重,有了 Heap Dump,问题就解决了一半。