Java 线上内存溢出(OOM)问题实战排查与解决全流程指南
在实际的生产环境中,Java 程序出现内存溢出(OutOfMemoryError, OOM)是非常常见但又极其棘手的问题。它不但会导致系统性能严重下降,甚至可能直接导致服务崩溃,影响业务的稳定运行。
本文将结合作者在生产环境中排查 JVM OOM 的真实案例,系统讲解如何识别问题、采集数据、使用分析工具(如阿里巴巴的 Arthas 和 VisualVM)定位原因,最终解决问题,避免故障重现。
一、问题背景:线上频繁出现 OOM
在网站上,部署的系统频繁在生产环境中隔一两天就出现一次 OOM(OutOfMemoryError)。每次出现问题后,系统不可用,需要人工重启服务,极大影响了用户体验与系统稳定性。
二、问题初步排查:确认 JVM 状况
1. 确定 Java 进程 ID
首先,我们通过如下命令查看系统中正在运行的 Java 进程:
jps
或使用更详细的命令:
ps -ef | grep java
假设输出结果显示博客程序的进程号为 17038。
2. 使用 jstat 查看 JVM 内存状态
接下来,通过 jstat 工具来查看该进程的 GC 状况和内存使用情况:
jstat -gc 17038 1000
每秒刷新一次,观察内存分区的使用百分比:
- Eden 区(新生代的伊甸园)
- S0/S1 区(新生代的 Survivor 区)
- Old 区(老年代)
问题发现:老年代使用率高达 99.8%,Full GC 次数频繁
这说明存在大量无法回收的对象长期驻留在老年代,最终导致老年代被撑爆,进而触发 OOM。
三、深入分析内存情况:Arthas 工具初探
1. Arthas 简介
Arthas 是阿里巴巴开源的一款强大的 Java 在线诊断工具,尤其适用于生产环境。
Arthas 的功能包括:
- 实时查看 JVM 内存、线程、GC 状态
- 查看类是从哪个 jar 包加载的
- 动态观察方法是否执行
- 方法执行耗时、异常堆栈分析
- Dump 堆内存,辅助定位内存泄漏
2. 安装与启动 Arthas
推荐使用 命令行一键安装:
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
启动后会自动列出本机所有 JVM 进程,例如:
* [1]: 17038 org.springframework.boot.loader.JarLauncher
输入对应编号(如 1),回车即可进入该进程的 Arthas 控制台。
3. 使用 dashboard 查看 JVM 概况
在 Arthas 命令行中输入:
dashboard
即可进入 JVM 实时仪表盘,输出包括以下信息:
- 当前线程数量与状态
- JVM 堆内存使用情况(堆总量、已用、最大)
- Eden 区 / Survivor 区 / Old 区 占比
- 非堆内存(方法区、Metaspace)使用情况
- GC 次数与时间统计
可直观判断是否存在内存泄漏或堆外内存问题
例如堆总量为 445MB,当前使用为 61MB,Eden 区、Survivor 区健康,但老年代占用极高,则极可能是老年代对象无法回收。
四、定位内存泄漏源:Heap Dump 文件分析
1. 使用 Arthas 导出堆内存快照
为了进一步分析哪个类或对象占用了大量内存,需要生成 .hprof 文件:
heapdump /tmp/dump1.hprof
这条命令将在 /tmp 目录下生成一份内存快照文件。
注意:
- 文件通常较大(几百 MB 到数 GB)
- 导出过程可能略微卡顿,请耐心等待
2. 下载 .hprof 文件至本地
使用工具(如 xftp, scp,或 SFTP 客户端)将服务器上 /tmp/dump1.hprof 文件下载到本地电脑。
3. 使用 VisualVM 打开并分析
VisualVM 是 Oracle 提供的 Java 性能分析工具,支持图形化展示 Heap Dump 内容。
步骤如下:
- 启动 VisualVM
- 菜单栏点击
File → Load - 选择刚才下载的
.hprof文件 - 等待加载并自动分析
4. 分析重点内容
打开 .hprof 文件后,关注以下几个方面:
- Top Consumers:占用内存最多的对象类型(按实例数量/字节排序)
- Class Histogram:各类对象的实例统计
- GC Roots 路径分析:查找强引用链条,判断是否为内存泄漏
- 实例内容查看:可进一步查看字段引用情况
如果发现某些缓存类、集合类(如
HashMap、ArrayList)数量异常,或第三方库中的类驻留时间异常,极可能是未释放资源或循环引用问题。
五、下一步:结合源代码深入剖析
由于 .hprof 文件中展示的是运行时内存的真实结构,结合代码逻辑能够更准确判断哪些对象未被释放:
- 是否存在静态集合缓存未清理?
- 是否线程池中存在任务未关闭?
- 是否定时任务持续创建对象却未释放?
- 是否数据库连接未关闭?
六、总结与建议
本次排查过程回顾:
| 步骤 | 工具 | 说明 |
|---|---|---|
| 1 | jps, jstat | 确认进程与初步内存状态 |
| 2 | Arthas - dashboard | 实时监控 JVM 内存/GC 状况 |
| 3 | Arthas - heapdump | 导出堆内存快照 |
| 4 | VisualVM | 图形化分析堆内存内容 |
| 5 | 源码阅读 | 结合代码逻辑分析引用路径 |
实战建议
- 开发阶段应避免长生命周期对象持有强引用
- 避免使用 static + Map 缓存无界数据
- 及时关闭数据库连接、线程池、IO流等资源
- 定期执行 GC 和监控 JVM 状态
- 生产环境部署 Arthas 等运维工具,确保诊断能力
以下是一篇详细整理与扩展后的文章,以便更系统、专业地讲解如何使用 VisualVM 工具配合 Dump 文件进行内存溢出(OOM)问题排查与解决。原始文稿中提到的所有知识点已保留并补充,结构清晰,内容可直接应用于线上生产环境调试。
使用 VisualVM 分析 OOM 内存溢出问题的完整实战流程
在复杂的 Java 应用运行过程中,OOM(OutOfMemoryError)是一类典型且棘手的问题。它往往不是短时间内大量内存申请造成的,而是程序长时间运行后,某些对象无法释放、持续堆积所引起的内存泄露。本篇将结合真实案例,介绍如何借助 VisualVM 加载 Dump 文件,定位导致内存溢出的元凶,进而解决问题。
一、初识 VisualVM 与 Heap Dump 文件
当 JVM 出现 java.lang.OutOfMemoryError: Java heap space 异常时,我们可以通过如下方式生成 堆转储(Heap Dump)文件:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
生成的 .hprof 文件可使用 VisualVM 工具进行可视化分析。打开后,主要分为以下几个关键区域:
二、VisualVM 中 Dump 文件的五大分析模块
1. 概览(Summary)面板
在 Summary(简介)面板中,我们可以看到以下基础统计信息:
- 当前堆内存总大小
- 已加载类的数量(如:19,000)
- 堆中实例化的对象总数(如:11,000,000+)
- 类加载器信息
- GC Root 跟踪起点数量
这些内容可帮助我们快速了解整个堆的对象分布与规模。如一个常见信号是:对象数量远超预期,比如本文案例中的 1100 万个对象就非常异常。
2. Environment(环境)信息
该部分列出了 JVM 所在的基础运行环境,如:
- 操作系统及其架构(是否为 64 位)
- JDK 安装路径与版本(如:HotSpot 1.8.0)
- JVM 运行时间
- JVM 启动参数
- 系统平台信息
这类静态信息通常用于排查跨平台兼容、JDK BUG 等低层问题。
3. Classes by Number of Instances(按实例数量查看类)
这是我们分析 OOM 的关键入口,以实例数量降序排列所有类。案例中发现:
HashMap$Node对象数量高达 200 万ConcurrentHashMap$Node实例数量超过 130 万
这是极不正常的,因为这两个类本身是 JDK 提供的内部结构,我们并未在业务代码中主动大量创建这类节点。
重点:这些 Node 并非我们显式创建的对象,说明可能是某些框架组件未正确释放相关资源。
4. Classes by Total Size(按实例总内存占用排序)
此视图帮助我们识别占用内存最大的对象类型。如:
HashMap$Node[]占用 113MBConcurrentHashMap$Node[]占用 60MB- 其他 Map/Node 类占用几十 MB
这类对象在数量最多的同时,也是最占内存的,说明它们可能就是内存泄露的源头。
5. Instances by Size(按单个实例大小排序)
该面板列出体积较大的单个对象(如大数组、缓存结构)。对于内存泄漏问题通常意义不大,但有助于分析大对象是否被滥用。
三、深入分析问题对象的引用关系
发现问题对象后(如 ConcurrentHashMap$Node),我们可以:
右键 → Open in New Tab
该操作将在新 Tab 中列出所有该类实例的引用结构,由于对象数量巨大,VisualVM 会分页处理,一般查看前几页即可。
展开某个具体实例,查看其字段:
key: 显示该 Map 的键对象value: 其对应的值对象- 若
value是业务对象,可以从中推断上下文
但很多时候,这些对象结构是框架内部创建的,不容易从业务层追踪其创建来源。
四、定位对象未被 GC 回收的根因:GC Root 跟踪
VisualVM 提供了一个重要功能:
选中对象 → 点右键 → Show Nearest GC Root
该功能能追溯该对象为何仍然存在于堆中,即:
哪个对象路径导致它仍被 GC Root 间接引用,因而无法被 GC 回收。
案例中发现:
- GC Root 最下方是一个类
common.Comm.IdleConnectionRipper - 包名包含
com.aliyun.oss,显然是阿里云 OSS SDK 组件
这说明:
当前对象并非业务代码创建,而是 OSS 客户端 SDK 中,由某个连接管理线程创建并未关闭,造成对象引用链断裂不掉。
五、进一步追踪具体线程与源码定位
在 GC Root 跟踪中,我们可以右键选择:
Select in Threads(在线程视图中定位)
VisualVM 会高亮该对象所在的线程及其执行栈。在案例中,发现其线程为:
com.aliyun.oss.common.Comm.IdleConnectionRipper.run() line: 78
源码分析显示,该线程处于 sleep 状态,但持有 OSS 客户端对象,导致所有通过它创建的 HTTP 连接都无法释放。
继续分析源码:
发现这段代码调用了:
this.idleConnectionManager.closeIdleConnections();
该方法应该周期性清理连接池中长时间未使用的连接,但由于未正确关闭 OSS 客户端,导致这些连接未被实际释放。
六、修复思路:主动释放资源
正确使用 OSS 客户端的建议:
OSSClient client = new OSSClient(endpoint, accessKeyId, accessKeySecret);
// 使用完成后,必须主动关闭:
client.shutdown();
案例中,由于未调用 shutdown() 方法,OSS SDK 背后的线程池与连接池未被关闭,导致内存不断累积,最终引发 OOM。
七、本地重现并验证问题解决
为验证该假设,作者执行以下步骤:
- 在本地重新启动应用,并通过 VisualVM 连接监控
- 在 Sampler → Memory 中实时查看内存中
HashMap$Node、ConcurrentHashMap$Node的数量变化 - 初始观察到这些对象数量持续增长,即使 GC 也无法回收
- 修改代码,增加
client.shutdown()释放资源 - 重启应用,再次通过 VisualVM 监控
- 每次手动 GC 后,相关对象数量明显下降
- 问题解决,堆内存恢复正常
八、经验总结:如何系统排查 OOM 问题
| 步骤 | 内容 |
|---|---|
| 1. 获取 Heap Dump 文件 | 使用 -XX:+HeapDumpOnOutOfMemoryError |
2. 用 VisualVM 打开 .hprof 文件 | 观察对象分布与异常 |
| 3. 识别对象异常类型与占用 | 根据数量、大小两种维度排序 |
| 4. 查看引用链 | 通过 GC Root 定位原因 |
| 5. 分析业务/三方代码 | 是否存在未关闭的连接、缓存、线程等 |
| 6. 验证假设 | 修改代码后本地重现 |
| 7. 应用修复到生产环境 | 打补丁上线并持续监控 |
九、常见导致内存泄漏的典型场景
| 场景 | 问题说明 |
|---|---|
| 数据库连接未关闭 | 导致连接池溢出 |
| HTTP 客户端未释放 | 与本案例 OSS 类似 |
| ThreadLocal 未清理 | 尤其在容器中容易遗留 |
| 过期缓存未清除 | map.put 后不再使用但无法回收 |
| 静态变量持有对象 | 造成生命周期过长 |
十、结语:工具 + 经验 + 推理 = OOM 问题终结者
调试 OOM 是一项需要 理论基础 + 实践技巧 + 推理能力 的综合能力。
本文所述的方法不仅适用于 OSS 客户端,也适用于任何三方库或业务代码造成的内存泄漏。关键在于:
- 熟悉 JVM 内存结构
- 掌握工具使用(VisualVM、MAT 等)
- 善于从 GC Root 推导出引用链并还原上下文
395

被折叠的 条评论
为什么被折叠?



