深入JVM:线上内存泄漏问题诊断与处理


深入JVM:线上内存泄漏问题诊断与处理

一、序言

对于Java工程师而言,深入理解JVM(Java虚拟机)不仅是掌握Java程序运行机制的基础,也是提升系统性能、优化应用和解决复杂问题能力的重要一步,更是Java进阶之路的重中之重。

本文小豪将贴近实战,带大家定位并处理内存泄漏问题,同时本文也将介绍目前较为流行的MAT内存分析工具的基本用法,以及其采用的支配树原理,相信阅读过本文之后,大家能够更深入地理解内存泄漏的检测方法和解决策略。

二、内存泄漏概念

在学习本文之前,建议先回顾上一篇【深入JVM:全面解析GC调优】,本文使用到的VisualVM监控工具,在上一篇中有做简要介绍。

在日常开发中,内存泄漏也是一个很常见的问题,首先我们需要明确一个概念,即内存泄漏和内存溢出是两种不同的内存管理问题,虽然它们最终都会导致堆内存的OOM,但它们并不对等。

  1. 内存泄漏:内存泄漏指的是某些对象已经不需要再使用,但其还在GC Root引用链上,垃圾回收器无法识别并回收这些对象,导致这其占用的内存无法被释放,可用的内存逐渐减少,最终导致OOM。内存泄漏基本都是代码逻辑上有问题,造成对象一直被错误地引用。
  2. 内存溢出:内存溢出指的是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 TreeHistogram

(2)Dominator Tree支配树

支配树是另一种比较重要的功能,MAT中支配树代表对象之间的支配关系,它不同于Java中GC Root的引用链

2.1 支配树原理

支配树是一种图形表示,在一张有向图中,确定一个起始点,如果起始点到终止点B的每条路径都经过A,那么称AB支配点(如下图,经过对象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了。

本文主要介绍的是内存泄漏问题的定位与解决方案,其实定位内存溢出的问题也同理,不过产生内存溢出的原因可能更为复杂一下,常见的有:

  1. 并发请求量过高,业务处理时占用大量内存
  2. 大文件报表导出等,一次性加载过多数据
  3. 堆内存空间分配过小

这些具体问题就要具体分析了,比如并发请求量过高可以引入中间件异步处理,进行限流保护,大文件报表可以选择分批导出,减少内存开销,当然在优化设计之后也要进行完整的测试验证。

五、后记

本文从内存泄漏的概念开始介绍,通过环境模拟,逐步带大家学习MAT内存分析工具定位并诊断内存泄漏的过程,同时额外引申出支配树的原理。

最后我们总结一下针对内存泄漏的处理流程:

  1. 获取到堆内存快照dump文件(关键)
  2. 借助内存分析工具(MAT等),导入dump文件智能诊断内存泄漏
  3. 定位到内存泄漏源头后,优化代码设计或调整技术方案

如果大家觉得内容有价值,不妨考虑点点赞,关注关注小豪,后续小豪将会继续更新JVM相关系列文章,大家共同进步~

相关推荐

  1. 内存泄漏内存溢出

    2024-06-10 05:06:02       31 阅读
  2. SpringBoot项目启动内存泄漏问题排查解决

    2024-06-10 05:06:02       21 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-10 05:06:02       5 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-10 05:06:02       5 阅读
  3. 在Django里面运行非项目文件

    2024-06-10 05:06:02       4 阅读
  4. Python语言-面向对象

    2024-06-10 05:06:02       6 阅读

热门阅读

  1. 窗帘怎么选好看不踩坑

    2024-06-10 05:06:02       19 阅读
  2. netty-学习

    2024-06-10 05:06:02       16 阅读
  3. Sylar---协程调度模块

    2024-06-10 05:06:02       13 阅读
  4. Redis命令实践

    2024-06-10 05:06:02       14 阅读
  5. C#根据反射生成sql语句(Update语句)

    2024-06-10 05:06:02       16 阅读
  6. HTTP-一

    HTTP-一

    2024-06-10 05:06:02      15 阅读
  7. 洛谷 P2926:轻拍牛头 ← 模拟题

    2024-06-10 05:06:02       15 阅读
  8. 自然语言处理(NLP)—— 自动摘要

    2024-06-10 05:06:02       19 阅读
  9. 我已经入驻@面包多平台

    2024-06-10 05:06:02       17 阅读
  10. # Mac环境如何安装Flutter:全面指南

    2024-06-10 05:06:02       20 阅读
  11. 第壹章第12节 C#和TS语言对比-多态

    2024-06-10 05:06:02       16 阅读