MAT(Memory Analyzer Tool
)工具是一款功能强大的]ava
堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。MAT是基于Eclipse
开发的,不仅可以单独使用,还可以作为插件的形式嵌入在Eclipse
中使用。是一款免费的性能分析工具,使用起来非常方便。
https://www.eclipse.org/mat/downloads.php
我目前电脑的JDK安装环境是1.8的,所以需要下载对应JDK1.8版本的MAT版本
下载后解压,点击MemoryAnalyzer.exe进行启动
问题描述:
要是直接下载最新版的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
问题描述:
有时候线上产生的堆dump文件较大,如果你的hprof文件没有问题的话,使用MAT打开的时候总是抛出 Java Heap Error. 可能是默认的1024m内存不够用了。
解决办法:
找到MAT的安装目录,找到MemoryAnalyzer.ini 修改其中的-Xmx即可
将-Xmx1024m 调大即可
jmap是Java虚拟机自带的一个命令行工具,可以用来生成JVM内存快照(Heap dump)文件。以下是使用jmap命令生成dump文件的步骤:
jmap -dump:format=b,file=heap.bin <pid>
通常情况下,在生产环境中使用jmap命令生成Heap dump文件时,建议把生成的文件下载到本地进行分析,以减少对生产环境的干扰。另外,在生成Heap dump文件时,一定要确保Java应用程序正常运行,否则可能会导致生成的文件不完整或者无法正确解析。
集群应用服务器在高并发请求的情况下会不定时地因为响应超时而报警,但是很快又超时解除,恢复正常,如此反复,让运维人员非常苦恼。
原因分析: 来到一家新公司,一个重构项目的开发人员估计搞不动了,最后选择跑路,我的到来正好接盘了这个有好多bug的项目。配合测试功能测试完结束后,进行压测。发现查询接口只要并发一起来就会出现错乱的现象。先是排查原先写的代码是否有问题,没发现问题,然后我以为是脏读,调整的事务的隔离级别等等方法,发现还是解决不了。最后没办法在方法上加了一个synchronized锁,再进行压测时,虽然吞吐量不高,但是不会有报错的现象。等开始正式切换系统进行上线时。因为每天会有至少20w的查询量。只要某个时间段只要请求量很高就会出现连接超时的现象。当时也想到是因为加了synchronized造成高并发请求下,很多请求一直在等待,最后因为时间太久而造成的超时。所以我就下载了dump文件,使用MAT工具进行分析。果然是这个锁造成的。这是的我已经对代码稍微熟悉了,分析什么造成的错乱现象。发现代码有一处用到了共享变量,造成每次高并发去请求出现的错乱现象。我当时心里。。。
参考https://blog.csdn.net/cl939974883/article/details/124581664 文档,确实是synchronized造成的
下面详说一下MAT如何分析dump文件
MAT
分析 OOM
问题通常思路:定位问题方式一:
现在有一个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
。
到现在为止,虽然没看程序代码,但是已经大概知道程序出现 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中,我们常见的数据包括: 运行日志、堆栈信息、GC信息、线程快照(threaddump/javacode)、堆快照(heapdump/hporf),jdk提供给我们了很实用的工具来分析,定位解决这些问题,这些工具包含于jdk中,并且以java实现,方便在不同的环境中不用安装其他依赖库即可使用,很是方便。下面分别介绍 jps、jinfo、jstat、jstack、jmap
,本文使用的jdk版本为1.8.0_11
配置项 | 作用 |
---|---|
-q | 忽略主类的名称,只输出pid |
-m | 输出启动类main函数的参数 |
-l | 输出主类名,如果进程执行的为jar,则输出jar路径 |
-v | 输出具体进程启动时jvm参数 |
1.命名格式
jps [options] pid
2.常用方式
1.常用用法
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
1、常用用法
效果演示: 会显示所有线程的各种信息,可以用来排查死锁,或线程长时间停滞的问题…