JVM OOM 分析与堆转储
OOM(OutOfMemoryError)是 JVM 内存耗尽的信号。不同区域 OOM 的原因差异巨大,掌握"判断类型 → 获取转储 → 定位泄漏"的完整链路,才能从堆转储中找到真正的元凶。
OOM 类型速查
| 异常信息 | 区域 | 常见原因 |
|---|---|---|
| Java heap space | 堆 | 对象堆积、缓存无界、泄漏 |
| Metaspace | 元空间 | 动态生成类、类加载器泄漏 |
| unable to create native thread | 线程栈 | 线程数超限、进程内存耗尽 |
| Direct buffer memory | 直接内存 | DirectByteBuffer 未释放 |
| GC overhead limit exceeded | 堆 | 反复 GC 仍无法回收,近似 OOM |
| Requested array size exceeds VM limit | 堆 | 申请超大数组 |
堆转储的获取
自动转储
JVM 抛出 OOM 时自动生成堆转储,需预先开启参数:
bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/logs/heapdump.hprof生产环境务必开启,OOM 一瞬间的堆状态最真实。
手动转储
bash
jmap -dump:format=b,file=heap.hprof <pid>- 会触发 STW,生产环境低峰期操作
-dump:live参数只保留存活对象,体积更小但会先 Full GC
转储后的第一步
bash
jmap -histo <pid> # 转储前先看对象分布,决定是否值得转储MAT:内存泄漏定位
MAT(Memory Analyzer Tool)是 Eclipse 出品的堆分析工具,专攻泄漏定位。
核心视图
| 视图 | 作用 |
|---|---|
| Leak Suspects | 一键给出"疑似泄漏"报告,按占用排序 |
| Dominator Tree | 支配树,展示谁"持有"了最多内存 |
| Histogram | 按类统计对象数与占用字节 |
| Paths to GC Roots | 从对象回溯到 GC Roots 的引用链 |
排查流程
1. 打开 hprof → 看 Leak Suspects 报告
2. 从占用最大的对象,右键 → Paths to GC Roots
3. 分析引用链:是集合未清理?静态引用?还是 ThreadLocal 持有?
4. 回到源码定位:缓存未设上限、监听器未注销、连接未关闭……关键技巧
- Path to GC Roots with all references:最常用,逐层看引用来源
- 对比两次转储:分别采集 GC 前后或不同时间点的转储,对比对象增长
- 关注
java.lang.ThreadLocal、HashMap、静态集合等"持有者"类型
JProfiler:热点与 CPU 分析
JProfiler 侧重运行期动态分析,通过 agent 附加到 JVM:
启动时附加:java -agentpath:/path/to/jprofiler/agent -jar app.jar
运行期附加:JProfiler GUI → Attach → 选择进程常用能力
| 能力 | 用途 |
|---|---|
| CPU Profiling | 定位热点方法与耗时瓶颈 |
| Allocation Profiling | 统计对象分配热点(哪个方法分配了最多对象) |
| GC Analysis | 实时 GC 活动与堆曲线 |
| Heap Walker | 类似 MAT 的堆浏览 |
| Thread Profiling | 线程状态与锁等待 |
场景:OOM 前的"对象分配速率异常"用 Allocation Profiling 定位到具体方法,比事后分析转储更直接。
VisualVM:轻量堆分析
VisualVM 随 JDK 附送(JDK 9 起独立分发),开箱即用的图形化工具:
bash
jvisualvm # JDK 8 直接运行
# JDK 9+ 需单独下载 VisualVM常用功能
| 功能 | 路径 |
|---|---|
| 堆直方图 | 监视 → 堆 → 生成堆转储 → 类视图 |
| GC 曲线 | 监视 → 堆/GC 活动 |
| 线程监控 | 线程 → 状态时间线 |
| 插件安装 | 工具 → 插件(含 Visual GC、TDA) |
小堆、快速验证场景用 VisualVM 足够;大堆、深度泄漏分析用 MAT 更专业。
典型排查案例
案例:List 无界增长
现象:OOM: Java heap space,Full GC 频繁
jmap -histo:char[] / String / 自定义对象数量巨大
MAT Paths to GC Roots:ArrayList ← 静态成员 ← 定时任务
结论:定时任务把每批数据 append 到静态 List,未清理案例:ThreadLocal 泄漏
现象:进程内存缓慢上涨,老年代持续增长
MAT 查看 ThreadLocal$ThreadLocalMap 大量 Entry
Paths to GC Roots:Thread → ThreadLocalMap → Entry → 大对象
结论:线程池中的线程未清理 ThreadLocal 值案例:Metaspace OOM
现象:OOM: Metaspace,类数量异常
jcmd <pid> VM.class_hierarchy 或 jmap -clstats 看类加载器
原因:热部署/动态代理每次生成新类,旧类加载器未释放排查方法论
- 先区分区域:OOM 信息直接告诉你是堆、元空间还是线程,方向完全不同
- 保留现场:OOM 前开启
HeapDumpOnOutOfMemoryError,转储是核心证据 - 从大到小:先看最大占用(Dominator Tree / Histogram),再沿引用链找持有者
- 看增长趋势:单次转储可能看不出泄漏,两次转储对比更可靠
- 工具选型:快速看 VisualVM,深度分析用 MAT,运行期定位分配热点用 JProfiler