JVM-性能优化工具 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[]几乎都是 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 的原因和大概的调用栈了。再贴出程序来对比一下,果然和我们看到得一模一样:
@SpringBootApplication public class OOMApplication implements CommandLineRunner { @Autowired FooService fooService; public static void main(String[] args) { SpringApplication.run(OOMApplication.class, args); } @Override public void run(String... args) throws Exception { //程序启动后,不断调用Fooservice.oom()方法 while (true) { fooService.oom(); } } } @Component public class FooService { List<String> data = new ArrayList<>(); public void oom() { //往同一个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 期望的幸存区大小
EC eden区大小
EU eden区已使用大小
YGC young gc次数
YGCT young gc 时间 (秒)
7、查看老年代GC情况
jstat -gcold pid
8、查看各空间容量
jstat -gccapacity pid
NGCMN 新生代最小容量
NGCMX 新生代最大容量
NGC 当前新生代容量
S0C survivor0大小
S1C survivor0大小
EC EDEN区大小
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 ) 虚拟机线程快照工具
效果演示: 会显示所有线程的各种信息,可以用来排查死锁,或线程长时间停滞的问题…