Java OOM 排查指南:工具、方法与最佳实践
Java OOM 排查指南:工具、方法与最佳实践
- 适用范围: JDK 8 / 11 / 17 / 21 · 生产环境故障处理 · JVM 调优
- 阅读时间: 约 20 分钟
目录
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 工具。
核心功能:
| 功能 | 说明 |
|---|---|
| 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 分析
情况四:线上不能停服,需要动态诊断
→ Arthas(memory / 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() |
| 数据库连接/流未关闭 | ResultSet、InputStream 未 close() |
使用 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 深度分析。