Java OOM 排查指南:工具、方法与最佳实践

4月 15, 2026 · 10 分钟阅读时长
blog

Java OOM 排查指南:工具、方法与最佳实践

文档信息
  • 适用范围: JDK 8 / 11 / 17 / 21 · 生产环境故障处理 · JVM 调优
  • 阅读时间: 约 20 分钟

目录

  1. OOM 类型速查
  2. 排查总体思路
  3. JVM 参数预设
  4. 核心诊断工具
  5. Heap Dump 分析
  6. 线上实战排查流程
  7. 常见场景与根因
  8. 预防与长效治理
  9. 参考资料

1. OOM 类型速查

Java OutOfMemoryError 并非单一错误,不同的错误消息指向完全不同的内存区域与根因。

错误消息 涉及区域 典型原因
Java heap space Java 堆 对象分配过多、内存泄漏
GC overhead limit exceeded Java 堆 GC 时间占比超 98%,回收率 < 2%
Metaspace 元空间(JDK 8+) 动态类加载过多、框架反射滥用
PermGen space 永久代(JDK 7-) 类/字符串常量池溢出
unable to create new native thread 本地内存 线程数超系统上限
Direct buffer memory 堆外内存 NIO DirectByteBuffer 未释放
Map failed 虚拟地址空间 内存映射文件过多
Compressed class space 压缩类空间 类数量超 CompressedClassSpaceSize
reason stack_trace_with_native_method 本地方法栈 JNI 调用栈溢出
诊断第一步

仔细阅读完整的 OOM 错误消息,不同的消息意味着完全不同的排查方向。


2. 排查总体思路

               ┌─────────────────────────────────────────┐
               │           应用抛出 OOM 异常               │
               └──────────────────┬──────────────────────┘
               ┌──────────────────▼──────────────────────┐
               │     Step 1: 确认 OOM 类型(错误消息)      │
               └──────────────────┬──────────────────────┘
          ┌───────────────────────┼───────────────────────┐
          │                       │                       │
    Heap OOM              Metaspace OOM           Thread/Native OOM
          │                       │                       │
   分析 Heap Dump          检查类加载器            检查线程数/堆外内存
          │                       │                       │
   找出泄漏对象            找出泄漏 ClassLoader      系统级排查 /proc
          │                       │                       │
          └───────────────────────┴───────────────────────┘
               ┌──────────────────▼──────────────────────┐
               │     Step 4: 修复代码 / 调整 JVM 参数       │
               └──────────────────┬──────────────────────┘
               ┌──────────────────▼──────────────────────┐
               │         Step 5: 验证 + 监控上线            │
               └─────────────────────────────────────────┘

3. JVM 参数预设

在部署阶段配置好以下参数,可在 OOM 发生时自动留存现场,是排查的先决条件。

3.1 堆内存与 GC

# 推荐生产基线(根据实际内存调整)
-Xms4g
-Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m

3.2 OOM 时自动 Dump

# 发生 OOM 时自动生成堆转储文件
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/app/logs/heapdump.hprof

# OOM 后执行自定义脚本(如告警/重启)
-XX:OnOutOfMemoryError="kill -9 %p; /app/scripts/restart.sh"

3.3 元空间限制

# 防止 Metaspace 无限增长(JDK 8+)
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

3.4 GC 日志(JDK 9+)

-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=50m

3.5 JDK 8 GC 日志

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/app/logs/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=50m

4. 核心诊断工具

4.1 jmap — 堆信息与 Dump 采集

jmap 是最直接的 JVM 堆分析入口。

# 查看堆统计摘要(不中断服务)
jmap -heap <pid>

# 查看堆对象直方图(按类型统计实例数和内存)
jmap -histo <pid>
jmap -histo:live <pid>   # 触发 Full GC 后统计,更精准

# 生成堆转储文件(线上慎用,会暂停 JVM)
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
jmap -dump:live,format=b,file=/tmp/heap-live.hprof <pid>
注意

jmap -dump 会触发 STW(Stop The World),对大堆(> 8 GB)影响显著,生产环境建议使用 jcmd 替代。


4.2 jcmd — 多功能命令行工具(推荐)

jcmd 是 JDK 7+ 引入的统一诊断接口,功能更完整,线上更安全。

# 列出所有 Java 进程
jcmd

# 查看进程支持的命令
jcmd <pid> help

# 生成堆转储(推荐替代 jmap)
jcmd <pid> GC.heap_dump /tmp/heap.hprof

# 查看 JVM 内存概况
jcmd <pid> VM.native_memory summary

# 查看类加载统计
jcmd <pid> VM.classloaders

# 查看 Metaspace 信息
jcmd <pid> VM.metaspace

# 强制触发 GC
jcmd <pid> GC.run

# 查看系统属性
jcmd <pid> VM.system_properties

# 查看 JVM 启动参数
jcmd <pid> VM.flags

4.3 jstat — 实时 GC 监控

# 每 1 秒输出一次 GC 统计,共 20 次
jstat -gcutil <pid> 1000 20

# 输出说明
# S0/S1 : Survivor 区使用率
# E     : Eden 区使用率
# O     : Old 区使用率
# M     : Metaspace 使用率
# YGC   : Young GC 次数
# YGCT  : Young GC 耗时(秒)
# FGC   : Full GC 次数
# FGCT  : Full GC 耗时(秒)
# GCT   : GC 总耗时

# 重点关注:FGC 频率与 O 区使用率持续接近 100%

4.4 jstack — 线程堆栈分析

排查 unable to create new native thread 类 OOM 时必备。

# 输出所有线程堆栈
jstack -l <pid> > /tmp/thread_dump.txt

# 统计线程数(快速判断是否线程泄漏)
jstack -l <pid> | grep "java.lang.Thread.State" | wc -l

# 查找特定状态线程
jstack -l <pid> | grep -A 5 "BLOCKED"
jstack -l <pid> | grep -A 5 "WAITING"

4.5 jinfo — 运行时 JVM 参数查看

# 查看所有 JVM 参数
jinfo -flags <pid>

# 查看单个参数值
jinfo -flag MaxHeapSize <pid>
jinfo -flag MetaspaceSize <pid>

# 动态修改可调整参数(无需重启)
jinfo -flag +HeapDumpOnOutOfMemoryError <pid>
jinfo -flag HeapDumpPath=/tmp/heap.hprof <pid>

4.6 MAT(Memory Analyzer Tool)— Heap Dump 深度分析

MAT 是分析 .hprof 文件的首选 GUI 工具。

下载: https://eclipse.dev/mat/

核心功能:

功能 说明
Leak Suspects Report 自动识别内存泄漏嫌疑对象,生成分析报告
Dominator Tree 按对象占用内存排序,快速定位"大对象"
Histogram 按类统计对象实例数与内存占用
OQL(对象查询语言) 类 SQL 语法查询堆中任意对象
Thread Overview 查看线程与栈帧中持有的对象引用
Path to GC Roots 追踪对象的引用链,找到 GC 无法回收的原因

典型分析步骤:

1. File → Open Heap Dump → 选择 .hprof 文件
2. 选择 "Leak Suspects Report" → 等待分析
3. 查看 "Problem Suspect" 列表,点击详情
4. 通过 "Path to GC Roots" 确认引用链
5. 结合业务代码定位根因

4.7 VisualVM — 轻量级可视化监控

内置于 JDK($JAVA_HOME/bin/jvisualvm)或独立下载: https://visualvm.github.io/

适合场景:

  • 开发/测试环境实时监控
  • 远程 JMX 连接生产进程(低侵入)
  • 快速查看堆/线程/CPU 概况

核心功能:

Monitor    → 实时查看堆/非堆/线程/CPU 趋势图
Threads    → 线程状态时间轴,直观发现阻塞
Heap Dump  → 一键触发并内置简单分析
Sampler    → CPU / 内存采样(非侵入式 profiling)
Profiler   → 精确性能分析(有一定开销)

4.8 Arthas — 线上诊断利器

阿里开源的 Java 诊断工具,无需重启、无需修改代码,适合生产环境热诊断。

快速启动:

# 下载并启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 选择目标进程后进入交互式 Shell

OOM 排查相关命令:

# 查看 JVM 内存概况(等同 jmap -heap)
memory

# 查看类加载统计
classloader

# 强制触发 GC
ognl "@java.lang.System@gc()"

# 实时监控方法调用(追踪大对象分配入口)
watch com.example.UserService loadUserList returnObj -x 3

# 追踪方法调用链路与耗时
trace com.example.DataService query

# 反编译类(确认线上代码版本)
jad com.example.CacheManager

# 查看对象内存占用
ognl -x 3 "@com.example.cache.LocalCache@instance"

# 热更新类(紧急修复,慎用)
redefine /tmp/FixedClass.class

4.9 GC 日志分析工具

工具 类型 特点
GCEasy (https://gceasy.io) Web 在线 上传日志自动生成可视化报告,免费版够用
GCViewer 桌面 GUI 开源,支持本地分析,功能较全
PerfMa (https://console.perfma.com) Web 在线 国内工具,支持 GC 日志 + Heap Dump 分析
JVM Sandbox Java Agent 阿里出品,生产级无侵入诊断框架

4.10 工具选型速查

OOM 发生时,我该用哪个工具?

情况一:需要快速判断内存趋势
   jstat -gcutil <pid> 1000

情况二:需要查看哪类对象最多
   jmap -histo:live <pid>

情况三:需要深度分析内存泄漏
   jcmd <pid> GC.heap_dump  MAT 分析

情况四:线上不能停服,需要动态诊断
   Arthasmemory / classloader / watch

情况五:线程数异常,怀疑线程泄漏
   jstack -l <pid>  统计线程数   BLOCKED/WAITING

情况六:Metaspace OOM,怀疑类加载泄漏
   jcmd <pid> VM.classloaders  MAT ClassLoader 分析

5. Heap Dump 分析

5.1 获取 Heap Dump 的多种方式

# 方式 1:jcmd(推荐)
jcmd <pid> GC.heap_dump filename=/tmp/heap.hprof

# 方式 2:jmap
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>

# 方式 3:JVM 参数自动触发
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/

# 方式 4:通过 JMX/VisualVM 界面触发

# 方式 5:通过 Arthas
heapdump /tmp/heap.hprof
heapdump --live /tmp/heap-live.hprof

5.2 MAT 分析要点

1. 检查 Leak Suspects

MAT 的自动泄漏检测通常能直接指出嫌疑类。重点关注:

  • 单个对象持有大量内存(> 堆总量的 10%)
  • 大量同类对象实例(如数万个 byte[]char[]

2. Dominator Tree 分析

Dominator Tree 展示"谁持有最多内存":
- 展开支配树,追踪到业务类
- 关注 Retained Heap 占比最高的节点
- 右键 → "Path to GC Roots" → "exclude weak/soft references"

3. OQL 查询示例

-- 查找所有 HashMap 实例及大小
SELECT s.@objectAddress, s.@retainedHeapSize FROM java.util.HashMap s
WHERE s.@retainedHeapSize > 1048576

-- 查找特定类的所有实例
SELECT * FROM com.example.UserSession

-- 查找大 byte 数组
SELECT s.@objectAddress, s.length FROM byte[] s WHERE s.length > 1000000

6. 线上实战排查流程

场景一:Heap OOM(最常见)

# Step 1: 确认 JVM 进程
jps -lvm | grep your-app-name

# Step 2: 查看 GC 压力
jstat -gcutil <pid> 2000 10
# 关注 O 区是否接近 100%,FGC 是否频繁

# Step 3: 查看对象分布
jmap -histo:live <pid> | head -30

# Step 4: 生成 Heap Dump
jcmd <pid> GC.heap_dump filename=/tmp/heap-$(date +%Y%m%d%H%M%S).hprof

# Step 5: 下载 Dump 到本地(如服务器无 GUI)
scp user@server:/tmp/heap-*.hprof ./

# Step 6: MAT 分析
# 打开 MAT → Leak Suspects Report → 定位根因

# Step 7: 修复后验证
jstat -gcutil <new-pid> 1000 60

场景二:Metaspace OOM

# Step 1: 确认 Metaspace 配置
jinfo -flag MaxMetaspaceSize <pid>

# Step 2: 查看类加载数量趋势
jstat -class <pid> 2000 20

# Step 3: 查看 ClassLoader 详情
jcmd <pid> VM.classloaders

# Step 4: MAT 分析类加载器
# MAT → Java Basics → Class Loader Explorer
# 找到持有大量类的 ClassLoader

# 常见根因:
# - 频繁创建新的 ClassLoader(动态代理、脚本引擎)
# - Spring CGLIB 代理类过多
# - JSP 编译类未清理

场景三:unable to create new native thread

# Step 1: 确认线程数
ps -eLf | grep java | wc -l
# 或
cat /proc/<pid>/status | grep Threads

# Step 2: 查看系统线程上限
ulimit -u
cat /proc/sys/kernel/threads-max

# Step 3: 分析线程堆栈
jstack -l <pid> > /tmp/thread_dump.txt
grep "java.lang.Thread.State" /tmp/thread_dump.txt | sort | uniq -c

# Step 4: 找出线程泄漏的代码
grep -B 20 "WAITING\|TIMED_WAITING" /tmp/thread_dump.txt | \
  grep "at com.example" | sort | uniq -c | sort -rn | head -20

# 常见根因:
# - 线程池未正确关闭,不断创建新线程
# - 每个请求创建新线程而非使用线程池
# - ThreadLocal 变量引发逻辑死锁导致线程堆积

场景四:Direct Buffer Memory OOM

# Step 1: 监控堆外内存
jcmd <pid> VM.native_memory summary

# Step 2: 查看 DirectBuffer 使用
ognl "@java.nio.Bits@reservedMemory"          # Arthas
# 或通过 JMX:java.nio:type=BufferPool,name=direct

# Step 3: 追踪 DirectByteBuffer 分配
jmap -histo:live <pid> | grep -i direct

# 常见根因:
# - Netty ByteBuf 未调用 release()
# - NIO Channel 未正确关闭
# - -XX:MaxDirectMemorySize 未设置(默认等于 -Xmx)

7. 常见场景与根因

7.1 内存泄漏模式

模式 典型代码 修复方案
静态集合无界增长 static List<Object> cache 只增不减 使用 WeakHashMap、LRU Cache 或设置容量上限
监听器未注销 eventBus.register(this) 无对应 unregister 实现 Lifecycle,在销毁时注销
ThreadLocal 未清理 线程池中 ThreadLocal 不调用 remove() finally 块中调用 threadLocal.remove()
数据库连接/流未关闭 ResultSetInputStreamclose() 使用 try-with-resources
Session/Cache 无过期 HTTP Session 或应用级缓存无 TTL 配置合理的过期策略与最大容量
大对象一次性加载 SELECT * 全表加载到内存 分页查询、流式处理(fetchSize
Hibernate 一级缓存膨胀 批处理中 Session 未定期 clear() 每批次调用 session.flush(); session.clear()

7.2 框架常见陷阱

Spring

  • @Async 方法使用默认 SimpleAsyncTaskExecutor(每次新建线程,无上限)
  • 循环依赖导致代理对象重复创建
  • @Cacheable 未配置缓存最大容量

Netty

  • ByteBuf 引用计数管理不当,release() 未成对调用
  • ChannelHandler@Sharable 却被多 Channel 共享

MyBatis

  • 大结果集 List 一次性加载
  • 未使用游标/流式查询处理百万级数据

8. 预防与长效治理

8.1 开发阶段

- [ ] Code Review 检查点:静态集合、ThreadLocal、流资源关闭
- [ ] 单元测试覆盖资源释放路径
- [ ] SonarQube 配置内存泄漏规则(resource-leak、thread-local)
- [ ] 避免使用 `Finalizer`,改用 `Cleaner`(JDK 9+)或显式释放

8.2 测试阶段

- [ ] 压测时开启 JVM 内存监控,观察 Old Gen 趋势
- [ ] 使用 JProfiler / YourKit 进行内存 Profiling
- [ ] 长时间压测(> 30 分钟),验证内存不持续增长
- [ ] 模拟高并发场景,验证线程池配置合理性

8.3 生产运维

- [ ] 配置 -XX:+HeapDumpOnOutOfMemoryError,确保现场可复现
- [ ] 接入 APM(SkyWalking、Pinpoint)监控 JVM 指标
- [ ] 设置告警阈值:Old Gen > 80% 触发告警,> 90% 触发 Heap Dump
- [ ] 定期 review GC 日志,关注 Full GC 频率趋势
- [ ] 容器环境配置 -XX:+UseContainerSupport(JDK 8u191+)

8.4 容器环境特别注意

# Docker/K8s 中 JVM 无法自动感知容器内存限制(JDK 8u131 之前)
# 必须显式配置或使用以下参数:

# JDK 8u191+ / JDK 10+(推荐)
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0

# 旧版 JDK 8 手动指定
-Xms1g -Xmx2g   # 根据容器 limit 的 75% 设置

9. 参考资料

资源 说明
Oracle JVM Troubleshooting Guide 官方故障排查手册
Eclipse MAT 文档 MAT 官方使用指南
Arthas 官方文档 阿里诊断工具完整文档
GCEasy 在线分析 GC 日志在线可视化
Java Performance: The Definitive Guide Scott Oaks 著,JVM 调优圣经
Understanding Java Garbage Collection Baeldung GC 系列教程

总结

OOM 排查的核心是"先确认类型,再定位区域,最后找代码根因"。生产环境务必提前配置 -XX:+HeapDumpOnOutOfMemoryError,确保故障现场可被捕获。工具链推荐:jstat 快速判断 → jcmd/Arthas 在线诊断 → MAT 深度分析