文章目录
- 深入JVM:线上内存泄漏问题诊断与处理
- 一、序言
- 二、内存泄漏概念
- 三、内存泄漏环境模拟
- 四、内存泄漏诊断与解决
- 1、步骤一:获取堆内存快照文件
- (1)获取正在运行程序dump文件
- (2)获取已终止程序dump文件
- 2、步骤二:诊断堆内存快照文件
- (1)MAT内存分析工具
- 3、步骤三:定位内存泄漏问题
- (1)Leak suspects内存泄漏报告
- (2)Dominator Tree支配树
- 2.1 支配树原理
- (3)Histogram直方图
- 4、步骤四:解决内存泄漏问题
- 五、后记
深入JVM:线上内存泄漏问题诊断与处理
一、序言
对于Java工程师而言,深入理解JVM(Java虚拟机)不仅是掌握Java程序运行机制的基础,也是提升系统性能、优化应用和解决复杂问题能力的重要一步,更是Java进阶之路的重中之重。
本文小豪将贴近实战,带大家定位并处理内存泄漏问题,同时本文也将介绍目前较为流行的MAT内存分析工具的基本用法,以及其采用的支配树原理,相信阅读过本文之后,大家能够更深入地理解内存泄漏的检测方法和解决策略。
二、内存泄漏概念
在学习本文之前,建议先回顾上一篇【深入JVM:全面解析GC调优】,本文使用到的VisualVM监控工具,在上一篇中有做简要介绍。
在日常开发中,内存泄漏也是一个很常见的问题,首先我们需要明确一个概念,即内存泄漏和内存溢出是两种不同的内存管理问题,虽然它们最终都会导致堆内存的OOM
,但它们并不对等。
- 内存泄漏:内存泄漏指的是某些对象已经不需要再使用,但其还在
GC Root
引用链上,垃圾回收器无法识别并回收这些对象,导致这其占用的内存无法被释放,可用的内存逐渐减少,最终导致OOM
。内存泄漏基本都是代码逻辑上有问题,造成对象一直被错误地引用。 - 内存溢出:内存溢出指的是JVM没有足够的内存空间满足对象的分配,导致内存溢出有可能是程序设计的问题,或者JVM参数配置的堆内存过小了,没有足够的内存空间。
这里我们借助VisualVM监控工具,观察一下堆内存正常使用曲线和异常使用曲线变化情况:
- 正常情况:堆内存使用呈锯齿形状,在执行垃圾回收之后,内存使用量会下降到一个较为平衡的位置,多次垃圾回收后下降的值接近,一般这种情况,代表程序内存使用正常
- 内存泄漏情况:堆内存使用呈逐步递增趋势,在执行垃圾回收之后,内存使用量的位置越来越高,直到使用满为止,这种情况就代表程序存在内存泄漏问题
三、内存泄漏环境模拟
这里我们模拟一个内存泄漏现象,提供对外接口,每次都将新创建的对象放置于static
静态变量集合中,始终保持引用:
public class FullGcController {
public static Map<String, byte[]> map;
// 简略内存泄漏
static {
map = new HashMap<>();
}
@GetMapping("/addMemory")
public void addMemory() {
// 随机ID
String autoId = UUID.randomUUID().toString();
// 创建对象,占用的内存大小为10M
byte[] memory = new byte[1024 * 1024 * 2];
map.put(autoId, memory);
}
}
然后使用Postman测试工具不断调用接口,直到产生OOM
:
java.lang.OutOfMemoryError: Java heap space
四、内存泄漏诊断与解决
在我们定位并解决内存泄漏问题之前,首先应该是先发生程序出现了内存泄漏,当然这部分工作更多是由系统运维、测试人员去完成的,比较专业一点的运维可能会采用目标比较流行的Prometheus + Grafana工具监控线上运行的程序,定期向我们反馈。
而我们开发调试中,也可用利用之前提到的VisualVM监控工具,在线观察内存使用变化。
但在绝大多数情况下,并不一定会有专门的运维人员监控线上程序,等出现内存泄漏问题时,往往是客户发现软件打不开了,我们跟踪到落盘日志时,才发现日志中打印出OutOfMemoryError
异常。
在这种情况下,就要求我们导出堆内存快照dump
文件,使用专业的内存分析工具定位内存泄漏:
1、步骤一:获取堆内存快照文件
首先第一步就是获取到堆内存快照dump
文件,这里分为两种情况,第一种是程序仍在运行,第二种是程序已经终止。
(1)获取正在运行程序dump文件
如果程序正在运行,获取dump
文件的方法还是比较多的。
- VisualVM:我们可用通过上面提到的VisualVM工具,点击[堆 Dump]快速导出
dump
文件:
VisualVM会返回生成的dump
文件路径所在位置:
- Jmap命令:jmap是JDK自带的命令行工具,用于生成JVM堆内存快照,具体用法也比较简单:
// 文件名以.hprof为后缀,pid为Java进程号
jmap -dump:format=b,file=文件名.hprof [pid]
(2)获取已终止程序dump文件
第二种情况即程序已经挂掉了,这就要求程序运行前在JVM启动命令中添加自动生成dump
文件的命令,这里需要添加两个命令:
// 当堆抛出OOM异常时,导出当前的堆内存快照
-XX:+HeapDumpOnOutOfMemoryError
// 指定生成堆内存快照的路径
-XX:HeapDumpPath=<路径>
当程序发生OOM
时,会在我们配置的指定路径下自动生成堆内存快照dump
文件。
2、步骤二:诊断堆内存快照文件
在导出堆内存快照dump
文件后,需要借助一些专业的内存分析工具帮我们智能诊断内存问题。
(1)MAT内存分析工具
MAT(Memory Analyzer Tool)是一款快速便捷且功能强大丰富的JVM堆内存离线分析工具,它是Eclipse开发工具的一个插件,一般我们独立下载MAT即可【官网在这】。
这里需要注意对应JDK与MAT的版本,小豪这里使用的是JDK 8,对应MAT的1.11版本:
同时由于堆内存快照dump
文件可能比较大,这里需要在MAT配置文件中调整其启动内存大小,一般建议调整为dump
文件的1.5
倍:
接着启动MAT,选择File -> Open Heap Dump导入dump
文件,默认选择Leak suspects Report,智能生成详细的内存泄漏报告。
3、步骤三:定位内存泄漏问题
(1)Leak suspects内存泄漏报告
生成后的内存泄漏报告如下:
针对我们模拟的内存泄漏问题,到这里其实MAT工具已经分析出来了,报告里显示FullGcController
类中的一个HashMap
实例对象占用了99.27%的内存,通过这些信息我们很容易就能定位到代码块,当然这个例子比较简单,实际业务中可能会较为复杂。
小豪在这里也补充MAT其它两个常用功能:Dominator Tree和Histogram
(2)Dominator Tree支配树
支配树是另一种比较重要的功能,MAT中支配树代表对象之间的支配关系,它不同于Java中GC Root
的引用链
2.1 支配树原理
支配树是一种图形表示,在一张有向图中,确定一个起始点,如果起始点到终止点B
的每条路径都经过A
,那么称A
是B
支配点(如下图,经过对象D
的路径都经过对象A
,则对象A
支配对象D
)。
通过支配树,MAT快速识别出哪些对象占用了大量的内存,这里有两个概念:
- Shallow Heap(浅堆):代表对象自身占用的内存
- Retained Heap(深堆):代表对象自身和其关联的对象占用的内存
MAT内存泄漏检测的原理其实主要就是依据支配树,若对象的深堆大小超过一定比例,则怀疑其为造成内存泄漏的对象
比如我们的FullGcController
对象自身只占用了8
字节,但其支配的对象占用了大量内存,基本就可以断定,问题出在FullGcController
对象中。
这里继续选择FullGcController
对象,右键点击[List objects],根据引用关系继续往下追,定位它具体引用了哪些大对象:
- with outgoing references:其引用的对象
- with incoming references:其被哪些对象引用
进一步定位到它里面占用内存最大的对象为map
:
(3)Histogram直方图
直方图主要展示所有类实例的大小:
大致用法与支配树类似,根据Retained Heap
深堆由大到小排列,分析具体占用内存较大的对象是谁。
4、步骤四:解决内存泄漏问题
至此我们已经定位到了代码中产生内存泄漏的对象了,剩下的就是优化设计修改代码。
小豪之前处理的一个线上内存泄漏问题的场景是这样的:当时我们的服务作为一个数据中台,接收其它厂商推过来的视频流地址,我们去解析推送过来的数据包。结果有个小伙伴先将接收到的数据包写入一个静态的阻塞队列Queue
,另外开启了一个线程监听此阻塞队列,但在从阻塞队列取出数据包消费后却没有将其删除,导致阻塞队列越积越多,果不其然最终OOM
了。
本文主要介绍的是内存泄漏问题的定位与解决方案,其实定位内存溢出的问题也同理,不过产生内存溢出的原因可能更为复杂一下,常见的有:
- 并发请求量过高,业务处理时占用大量内存
- 大文件报表导出等,一次性加载过多数据
- 堆内存空间分配过小
这些具体问题就要具体分析了,比如并发请求量过高可以引入中间件异步处理,进行限流保护,大文件报表可以选择分批导出,减少内存开销,当然在优化设计之后也要进行完整的测试验证。
五、后记
本文从内存泄漏的概念开始介绍,通过环境模拟,逐步带大家学习MAT内存分析工具定位并诊断内存泄漏的过程,同时额外引申出支配树的原理。
最后我们总结一下针对内存泄漏的处理流程:
- 获取到堆内存快照
dump
文件(关键) - 借助内存分析工具(MAT等),导入
dump
文件智能诊断内存泄漏 - 定位到内存泄漏源头后,优化代码设计或调整技术方案
如果大家觉得内容有价值,不妨考虑点点赞,关注关注小豪,后续小豪将会继续更新JVM相关系列文章,大家共同进步~