一、MAT下载和安装
1、概述
MAT(
Memory Analyzer Tool
)工具是一款功能强大的
]ava
堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。MAT是基于
Eclipse
开发的,不仅可以单独使用,还可以作为插件的形式嵌入在
Eclipse
中使用。是一款免费的性能分析工具,使用起来非常方便。
2、下载地址:
https://www.eclipse.org/mat/downloads.php
我目前电脑的JDK安装环境是1.8的,所以需要下载对应JDK1.8版本的MAT版本
3、安装
下载后解压,点击MemoryAnalyzer.exe进行启动
4、安装出现的报错问题
4.1、MAT版本和JDK版本不一致
问题描述:
要是直接下载最新版的MAT,可能需要高版本JDK才行。启动是需要JDK11或者更高的版本,我本地JDK版本是1.8,所以会报JDK版本不适合。
解决方法:
在MemoryAnalyzer.ini 中加入指定jdk的地址, (jdk不用安装直接下载 解压指定bin/javaw.exe就可)
-vm
D:/java/jdk1.8.0_211/binbin/javaw.exe
-vm
D:/java/jdk1.8.0_211/binbin/javaw.exe
-startup
plugins/org.eclipse.equinox.launcher_1.5.0.v20180512-1130.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.700.v20180518-1200
-vmargs-Xmx1024m
4.2、堆dump文件较大、使用MAT打开的时候总是抛出 Java Heap Error
问题描述:
有时候线上产生的堆dump文件较大,如果你的hprof文件没有问题的话,使用MAT打开的时候总是抛出 Java Heap Error. 可能是默认的1024m内存不够用了。
解决办法:
找到MAT的安装目录,找到MemoryAnalyzer.ini 修改其中的-Xmx即可
将-Xmx1024m 调大即可
5、jmap命令拿到dump日志文件
jmap是Java虚拟机自带的一个命令行工具,可以用来生成JVM内存快照(Heap dump)文件。以下是使用jmap命令生成dump文件的步骤:
jmap -dump:format=b,file=heap.bin <pid>
通常情况下,在生产环境中使用jmap命令生成Heap dump文件时,建议把生成的文件下载到本地进行分析,以减少对生产环境的干扰。另外,在生成Heap dump文件时,一定要确保Java应用程序正常运行,否则可能会导致生成的文件不完整或者无法正确解析。
二、MAT工具排查分析OOM
1、故障现象:
集群应用服务器在高并发请求的情况下会不定时地因为响应超时而报警,但是很快又超时解除,恢复正常,如此反复,让运维人员非常苦恼。
原因分析: 来到一家新公司,一个重构项目的开发人员估计搞不动了,最后选择跑路,我的到来正好接盘了这个有好多bug的项目。配合测试功能测试完结束后,进行压测。发现查询接口只要并发一起来就会出现错乱的现象。先是排查原先写的代码是否有问题,没发现问题,然后我以为是脏读,调整的事务的隔离级别等等方法,发现还是解决不了。最后没办法在方法上加了一个synchronized锁,再进行压测时,虽然吞吐量不高,但是不会有报错的现象。等开始正式切换系统进行上线时。因为每天会有至少20w的查询量。只要某个时间段只要请求量很高就会出现连接超时的现象。当时也想到是因为加了synchronized造成高并发请求下,很多请求一直在等待,最后因为时间太久而造成的超时。所以我就下载了dump文件,使用MAT工具进行分析。果然是这个锁造成的。这是的我已经对代码稍微熟悉了,分析什么造成的错乱现象。发现代码有一处用到了共享变量,造成每次高并发去请求出现的错乱现象。我当时心里。。。
参考https://blog.csdn.net/cl939974883/article/details/124581664 文档,确实是synchronized造成的
下面详说一下MAT如何分析dump文件
2、
MAT
分析
OOM
问题通常思路:
- 通过支配树功能或直方图功能查看消耗内存最大的类型,来分析内存泄露的大概原因;
- 查看那些消耗内存最大的类型、详细的对象明细列表,以及它们的引用链,来定位内存泄露的具体点;
- 配合查看对象属性的功能,可以脱离源码看到对象的各种属性的值和依赖关系,帮助我们理清程序逻辑和参数;
- 辅助使用查看线程栈来看 OOM 问题是否和过多线程有关,甚至可以在线程栈看到 OOM 最后一刻出现异常的线程。
3、使用MAT定位问题:
- 定位问题方式一:现在有一个
OOM
后得到的堆转储文件 java_pid29569.hprof,现在要使用 MAT 的直方图
、支配树
、线程栈
、OQL
等功能来分析此次 OOM 的原因。首先,用 MAT 打开后先进入的是概览信息界面,可以看到整个堆是 437.6MB: 那么,这 437.6MB 都是什么对象呢?如图所示,工具栏的第二个按钮可以打开直方图,直方图按照类型进行分组,列出了每个类有多少个实例,以及占用的内存。可以看到,char[]字节数组占用内存最多,对象数量也很多,结合第二位的 java.lang.String 类型对象数量也很多,大概可以猜出(String 使用 char[]作为实际数据存储)程序可能是被字符串占满了内存,导致 OOM。 在char[]
上点击右键,选择List objects->with incoming references
,就可以列出所有的char[]
实例,以及每个char[]
的整个引用关系链: 随机展开一个 char[],如下图所示: 接下来,我们按照红色框中的引用链来查看,尝试找到这些大char[]
的来源: - 在①处看到,这些
char[]
几乎都是 10000 个字符、占用 20000 字节左右(char 是 UTF-16,每一个字符占用 2 字节); - 在②处看到,char[]被
String
的value
字段引用,说明 char[]来自字符串; - 在③处看到,
String
被ArrayList
的elementData
字段引用,说明这些字符串加入了一个ArrayList
中; - 在④处看到,
ArrayList
又被FooService
的data
字段引用,这个ArrayList
整个RetainedHeap
列的值是 431MB。 - 左侧的蓝色框可以查看每一个实例的内部属性,图中显示
FooService
有一个data
属性,类型是ArrayList
。**Retained Heap(深堆)
:代表对象本身和对象关联的对象占用的内存;****Shallow Heap(浅堆)
:代表对象本身占用的内存。** 比如,我们的 FooService 中的 data 这个 ArrayList 对象本身只有 16 字节,但是其所有关联的对象占用了 431MB 内存。这些就可以说明,肯定有哪里在不断向这个 List 中添加 String 数据,导致了 OOM。如果我们希望看到字符串完整内容的话,可以右键选择 Copy->Value,把值复制到剪贴板或保存到文件中: 这里,我们复制出的是 10000 个字符 a(下图红色部分可以看到)。 看到这些,我们已经基本可以还原出真实的代码是怎样的了,定位到了问题代码。 - 定位问题方式二: 其实,我们之前使用直方图定位
FooService
,已经走了些弯路。你可以点击工具栏中第三个按钮(下图左上角的红框所示)进入支配树界面。这个界面会按照对象的Retained Heap
倒序直接列出占用内存最大的对象。 可以看到,第一位就是FooService
,整个路径是FooSerice->ArrayList->Object[]->String->char[]
(蓝色框部分),一共有 21523 个字符串(绿色方框部分)。通常使用这种方式可以一步到位的定位出问题所在。 - 借助MAT寻到具体问题原因 我们就从内存角度定位到
FooService
是根源了。那么,OOM
的时候,FooService
是在执行什么逻辑呢?为解决这个问题,我们可以点击工具栏的第五个按钮(下图红色框所示)。打开线程视图,首先看到的就是一个名为 main 的线程(Name 列),展开后果然发现了FooService
: 先执行的方法先入栈,所以线程栈最上面是线程当前执行的方法,逐一往下看能看到整个调用路径。 - 因为我们希望了解
FooService.oom()
方法,看看是谁在调用它,它的内部又调用了谁,所以选择以FooService.oom()
方法(蓝色框)为起点来分析这个调用栈。 - 往下看整个绿色框部分,
oom()
方法被OOMApplication
的run
方法调用,而这个run
方法又被SpringAppliction.callRunner
方法调用。 - 看到参数中的
CommandLineRunner
你应该能想到,OOMApplication
其实是实现了CommandLineRunner
接口,所以是SpringBoot
应用程序启动后执行的。 - 以
FooService
为起点往上看,从紫色框中的Collectors
和IntPipeline
,大概也可以猜出,这些字符串是由Stream
操作产生的。 - 再往上看,可以发现在
StringBuilder
的append
操作的时候,出现了OutOfMemoryError
异常(黑色框部分),说明这这个线程抛出了OOM
异常。我们看到,整个程序是Spring Boot
应用程序,那么FooService
是不是Spring
的Bean
呢,又是不是单例呢?如果能分析出这点的话,就更能确认是因为反复调用同一个FooService
的oom
方法,然后导致其内部的ArrayList
不断膨胀。点击工具栏的第四个按钮(如下图红框所示),来到OQL
界面。在这个界面,我们可以使用类似SQL
的语法,在dump
中搜索数据(你可以直接在MAT
帮助菜单搜索OQL Syntax
,来查看OQL
的详细语法)。 比如,输入如下语句搜索 FooService 的实例:SELECT*FROM org.geekbang.time.commonmistakes.troubleshootingtools.oom.FooService
可以看到只有一个实例,然后我们通过 List objects 功能搜索引用 FooService 的对象: 可以看到,一共两处引用: - 第一处是,
OOMApplication
使用了FooService
。 - 第二处是一个
ConcurrentHashMap
。可以看到,这个HashMap
是DefaultListableBeanFactory
的singletonObjects
字段,可以证实FooService
是Spring
容器管理的单例的Bean
。
4、结论
到现在为止,虽然没看程序代码,但是已经大概知道程序出现 OOM 的原因和大概的调用栈了。再贴出程序来对比一下,果然和我们看到得一模一样:
@SpringBootApplicationpublicclassOOMApplicationimplementsCommandLineRunner{@AutowiredFooService fooService;publicstaticvoidmain(String[] args){SpringApplication.run(OOMApplication.class, args);}@Overridepublicvoidrun(String... args)throwsException{//程序启动后,不断调用Fooservice.oom()方法while(true){
fooService.oom();}}}@ComponentpublicclassFooService{List<String> data =newArrayList<>();publicvoidoom(){//往同一个ArrayList中不断加入大小为10KB的字符串
data.add(IntStream.rangeClosed(1,10_000).mapToObj(__ ->"a").collect(Collectors.joining("")));}}
到这里,我们使用 MAT 工具从对象清单、大对象、线程栈等视角,分析了一个 OOM 程序的堆转储。可以发现,有了堆转储,几乎相当于拿到了应用程序的源码 + 当时那一刻的快照,OOM 的问题无从遁形。
三、jvm-jps、jinfo、jstat、jstack、jmap 基本使用
给系统定位问题的时候,知识经验是基础,应用数据是依据,工具是手段,在jvm中,我们常见的数据包括: 运行日志、堆栈信息、GC信息、线程快照(threaddump/javacode)、堆快照(heapdump/hporf),jdk提供给我们了很实用的工具来分析,定位解决这些问题,这些工具包含于jdk中,并且以java实现,方便在不同的环境中不用安装其他依赖库即可使用,很是方便。下面分别介绍
jps、jinfo、jstat、jstack、jmap
,本文使用的jdk版本为1.8.0_11
1、jps ( jvm process status tool ) 虚拟机进程工具
配置项作用-q忽略主类的名称,只输出pid-m输出启动类main函数的参数-l输出主类名,如果进程执行的为jar,则输出jar路径-v输出具体进程启动时jvm参数
1.命名格式
jps [options] pid
2.常用方式
- jps -lv : 输出启动类名与启动时jvm参数,可以方便的看到各个tomcat的自定义参数配置
- jps -lv |grep project_name : 在上述基础上过滤出自己想要查看的项目的信息
2、jinfo ( configuration info for java ) 显示虚拟机配置信息
1.常用用法
- jinfo pid : 显示jvm系统属性与vm参数信息
- jinfo -flags pid : 显示jvm vm参数信息,如最大最小堆,默认堆,垃圾收集器参数等
- jinfo -sysprops pid : 显示jvm系统属性
- jinfo -flag : 显示特定vm参数值,例如 jinfo -flag MaxHeapSize pid 输出pid的最大堆内存
3、jstat ( jvm statistics monitoring tool) 收集虚拟机各方面运行数据
1、语法格式
jstat [ option pid [interval[s|ms][count]]]
说明: interval 表示循环时间间隔,默认单位为ms,可以在直接使用s/ms指定单位,如 60ms/1s, count 表示输出几次 例:
jstat gc pid 1s 20
每1s查询一次gc情况,查询20次
2、option 详解(主要分三类:类装载、垃圾收集、运行期编译状况)
配置项作用-class监视类装载、卸载数量、总空间以及类装载所耗费的时间-gc监视Java堆,包括Eden区、两survivor区、老年代、永久代等的容量、已用空间、GC时间合计等-gccapacity与-gc基本相同,但关注点为Java堆各个区域使用到的最大、最小空间-gcutil与-gc基本相同,但关注点为Java堆各个区域已使用空间占总空间的百分比-gccause与-gcutil功能相同,但会额外输出导致上一次GC产生的原因-gcnew监控新生代GC情况-gcnewcapacity与-gcnew基本相同,但关注最大,最小空间-gold监控老年代GC情况-goldcapacity与-gcold基本相同,但关注最大,最小空间-compiler输出被JIT编译过的方法、耗时等信息-printcomplilation输出已经被JIT编译的方法
3、查看类装载卸载情况 jstat -class pid
属性释义Loaded装载总数量Bytes装载总大小Unloaded卸载类的数量Time加载和卸载类总共的耗时
4、查看GC情况 jstat -gc pid
属性释义S0C新生代survivor0容量S1C新生代survivor1容量S0U新生代survivor0已使用大小S1U新生代survivor1已使用大小EC新生代eden区容量EU新生代eden区已使用大小OC老年代容量OU老年代已使用大小MC元数据容量,即方法区容量MU元数据已使用空间CCSC压缩类空间大小CCSU压缩类空间使用大小YGC新生代gc次数(young gc)YGCT新生代gc时间(s)FGC老生代gc次数(full gc)GCT总的gc时间,包括young gc和full gc
5、查看GC情况,以百分比显示
jstat -gcutil pid
6、查看新生代GC情况
jstat -gcnew pid
属性释义S0C新生代survivor0容量S1C新生代survivor1容量S0U新生代survivor0已使用大小S1U新生代survivor1已使用大小TT对象在新生代存活的次数MTT对象在新生代存活的最大次数DSS期望的幸存区大小ECeden区大小EUeden区已使用大小YGCyoung gc次数YGCTyoung gc 时间 (秒)
7、查看老年代GC情况
jstat -gcold pid
8、查看各空间容量
jstat -gccapacity pid
属性释义NGCMN新生代最小容量NGCMX新生代最大容量NGC当前新生代容量S0Csurvivor0大小S1Csurvivor0大小ECEDEN区大小OGCMN老年代最小容量OGCMX老年代最大容量OGC当前老年代大小MCMN元数据最小容量MCMX元数据最大容量CCSMN最小压缩类空间大小CCSMX最大压缩类空间大小CCSC当前压缩类空间大小)
9、查看编译情况
jstat -compiler pid
4、jmap ( memory map for java ) 虚拟机堆快照工具
1、常用用法
- jmap -heap pid 查看当前jvm heapdump与垃圾收集器的使用情况
- jmap -dump:format=b,file=/temp/filename.hprof pid 转储堆快照,生成hprof文件到指定路径
- jmap -histo pid 列出当前heap中对象状况,附字节码与java对象映射表
5、jstack ( stack trace for java ) 虚拟机线程快照工具
效果演示: 会显示所有线程的各种信息,可以用来排查死锁,或线程长时间停滞的问题…
版权归原作者 代码的知行者 所有, 如有侵权,请联系我们删除。