本文梳理线上 OOM 问题的完整排查链路:从获取 Heap Dump,到用 JProfiler 定位内存大户,再到沿引用链追溯 GC Root,最后结合业务代码确认根因。
一、什么是 OOM
OOM(OutOfMemoryError)是 JVM 在无法分配足够内存时抛出的错误。常见类型:
| OOM 类型 | 原因 |
|---|---|
Java heap space | 堆内存不足,最常见的 OOM |
GC overhead limit exceeded | GC 耗时过长但回收内存极少,JVM 判定陷入死循环 |
Metaspace | 元空间(类信息)占用过大,通常由类加载过多或框架动态生成类导致 |
Direct buffer memory | NIO 直接内存(堆外内存)耗尽 |
unable to create new native thread | 线程数超过操作系统限制 |
二、获取 Heap Dump 文件
分析 OOM 的第一步是拿到 Heap Dump 文件(.hprof 或无后缀文件)。
2.1 JVM 启动参数自动生成
在启动参数中添加以下配置,OOM 发生时自动生成 dump 文件(强烈建议线上服务默认开启):
1 | -XX:+HeapDumpOnOutOfMemoryError |
2.2 手动生成(对运行中的进程)
1 | # 查找 Java 进程 PID |
2.3 通过 Arthas 生成
1 | java -jar arthas-boot.jar <PID> |
三、分析工具选择
拿到 dump 文件后,有两种主流的 JProfiler 分析方式:
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JProfiler 软件 | 功能完整,分析视图丰富 | 需单独安装,上手略复杂 | 深度分析,追踪复杂引用链 |
| IDEA JProfiler 插件 | 可直接定位到代码行,操作简便 | 功能相对精简 | 快速定位,日常排查 |
其他常用工具:Eclipse MAT(内存分析专用,适合分析超大 dump 文件)、VisualVM(轻量,免费开源)。
四、JProfiler 软件分析
4.1 打开 Dump 文件
打开 JProfiler → 启动中心 → 打开单个快照 → 选择 dump 文件。
常见问题:文件选择框中找不到 dump 文件
- 原因:文件选择框有文件类型过滤,限制了可见的后缀,而 dump 文件可能无后缀。
- 解决:在文件路径输入框中直接输入文件的完整路径或文件名,回车确认即可打开。
4.2 类视图(Heap Walker → Classes)
⚠️ 常见误区:排在最上面的类不一定是导致 OOM 的元凶!
JProfiler 默认按实例计数排序,实例多 ≠ 内存占用大。例如ConcurrentHashMap$Node实例数极多,但每个实例很小,总内存未必最大。
正确分析步骤:
- 按”大小(浅堆)”排序:快速看到哪些类的实例总体积最大。例如
char[]和byte[]加起来占用约 1.6G,明显异常。 - 计算保留大小:点击工具栏”计算预估的保留大小”,等待计算完成。
- 按”保留大小”排序:定位真正的内存大户。例如
WsFrameServer保留大小达 1.5G,它是 Tomcat 处理 WebSocket 数据帧的核心类,由此定位到 WebSocket 连接未释放的问题。
“大小”与”保留大小”的区别
这是分析 OOM 最核心的两个概念:
| 指标 | 别名 | 定义 |
|---|---|---|
| 大小 | 浅堆(Shallow Heap) | 对象本身在堆中占用的空间,不包括它引用的其他对象 |
| 保留大小 | 保留堆(Retained Heap) | 对象被 GC 后能释放的总内存 = 对象本身 + 所有被它独家持有的对象 |
举例理解:
1 | 对象 A(浅堆 = 16B) |
分析 OOM 时,保留大小才是最关键的指标——它代表”释放这个对象能收回多少内存”。
4.3 最大对象视图(Biggest Objects)
| 模式 | 排序方式 | 适用场景 | 局限 |
|---|---|---|---|
| 不分组(默认) | 按单个对象实例的保留大小 | 怀疑是某个特定的单一超大对象(如一个超大 Map、List) | 无法发现海量小对象累积 |
| 按类分组 | 同一类所有实例的保留大小总和 | 定位海量小对象累积(如大量 Session、Request 未释放) | — |
在类视图中双击某个类,会创建一个新的对象集(Object Set),仅包含该类的所有实例,便于进一步分析。
4.4 引用视图(References)
| 引用类型 | 核心问题 | 主要用途 |
|---|---|---|
| 传出引用 | “这个对象里面有什么?” | 分析巨大对象的内部构成 |
| 合并的传出引用 | “这个集合里装了哪些类型的对象?” | 分析集合类对象,避免视图臃肿 |
| 传入引用 | “是谁持有着这个对象,阻止它被 GC?” | 诊断内存泄漏最常用 |
| 合并的传入引用 | “哪些组件在共享这个对象?” | 追溯被多方引用的共享对象 |
分析内存泄漏时,沿着传入引用向上追溯,直到找到 GC Root,就能定位是谁”拎着”这个对象不放。
五、IDEA JProfiler 插件分析
将 dump 文件直接拖入 JProfiler 面板即可。核心功能入口:
| 功能 | 说明 |
|---|---|
| GC Roots | 所有直接作为 GC Root 的对象,按类型分组,用于诊断内存泄漏 |
| Merged Paths(Dominator Tree) | 支配树。A 支配 B = 所有访问 B 的路径都必须经过 A;GC 掉 A,A 支配的所有对象都可回收 |
| Packages | 按 Java 包分组统计的旭日图,快速定位问题所在的包范围 |
选中某个实例后,下方分析选项卡中,Shortest Paths(从 GC Roots 到此对象的最短引用路径)是定位内存泄漏根因的利器。
六、OOM 分析通用思路
1 | Step 1: 按"保留大小"排序 → 找到最占内存的类/对象 |
七、常见 OOM 场景与排查方向
| 场景 | dump 特征 | 排查方向 |
|---|---|---|
| 缓存无限增长 | 大量 HashMap/ConcurrentHashMap 及 Entry | 检查缓存是否设置容量上限和过期策略 |
| WebSocket/长连接泄漏 | WsFrameServer、Channel 保留大小极大 | 检查连接关闭逻辑、心跳超时处理 |
| 大量 Session 未释放 | 海量 HttpSession 对象 | 检查 Session 超时配置和销毁逻辑 |
| 大字符串/byte 数组 | char[]、byte[] 占用极大 | 检查大文件读取未分片、大 SQL 结果集未分页 |
| 线程池任务队列积压 | 大量 Runnable/Task 对象 | 检查线程池队列容量和任务执行速度 |
| 类加载过多(Metaspace) | Metaspace 溢出,类数量异常多 | 检查动态代理、脚本引擎、热部署是否产生类泄漏 |
八、总结
| 要点 | 说明 |
|---|---|
| 核心指标 | 保留大小(Retained Heap)是分析 OOM 最关键的指标 |
| 分析入口 | 从”类视图 → 按保留大小排序”开始,快速缩小范围 |
| 引用追踪 | 使用传入引用 / Shortest Paths,向上找到阻止 GC 的根因 |
| 工具选择 | 快速定位用 IDEA 插件;深度分析用 JProfiler 软件或 MAT |
| 预防措施 | 设置 -XX:+HeapDumpOnOutOfMemoryError 确保 OOM 时自动保留现场 |



