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

2024-06-10 05:28

本文主要是介绍深入JVM:线上内存泄漏问题诊断与处理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 深入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,但它们并不对等。

  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() {// 随机IDString autoId = UUID.randomUUID().toString();// 创建对象,占用的内存大小为10Mbyte[] 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相关系列文章,大家共同进步~

这篇关于深入JVM:线上内存泄漏问题诊断与处理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1047320

相关文章

linux生产者,消费者问题

pthread_cond_wait() :用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。 pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

2024.6.24 IDEA中文乱码问题(服务器 控制台 TOMcat)实测已解决

1.问题产生原因: 1.文件编码不一致:如果文件的编码方式与IDEA设置的编码方式不一致,就会产生乱码。确保文件和IDEA使用相同的编码,通常是UTF-8。2.IDEA设置问题:检查IDEA的全局编码设置和项目编码设置是否正确。3.终端或控制台编码问题:如果你在终端或控制台看到乱码,可能是终端的编码设置问题。确保终端使用的是支持你的文件的编码方式。 2.解决方案: 1.File -> S

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前