【JVM调优】MAT工具使用
一、功能整体示意
https://blog.csdn.net/x275920/article/details/123991656
https://www.cnblogs.com/lvxueyang/p/14833614.html (深堆,浅堆,OQL语言)
https://blog.csdn.net/lyd135364/article/details/121449969 (MAT工具使用)
https://blog.csdn.net/Dongguabai/article/details/123425128 (老年代内存工具查看)
http://www.manongjc.com/detail/28-mzjipprdmlptlzw.html(老年代内存工具查看)
https://www.cnblogs.com/ylz8401/p/15937892.html(老年代内存工具查看)
https://blog.csdn.net/a15835774652/article/details/125479631(mac上安装mat)
https://www.cnblogs.com/whaleX/p/14059972.html(修改MAT的内存配置)
https://zhuanlan.zhihu.com/p/535069800(MAT介绍)
http://wjhsh.net/wcss-p-12067606.html(OQL语言)
shallow heap 的大小单位是字节
Retained heap 的大小单位是字节
堆转储快照信息
jmap -dump:format=b,file=/home/heap.hprof 217533
查看老年代的对象信息(对象地址可以从GC日志中获取) SELECT * FROM INSTANCEOF java.lang.Object t WHERE (toHex(t.@objectAddress) >= "0x720000000" AND toHex(t.@objectAddress) <= "0x7c0000000") 0x0000000720000000 0x00000007c0000000 0x00000007c0000000
1.1、获取dump文件的方式
获取堆快照 dump 文件(堆转储需要先执行 Full GC,线上服务使用时请注意影响),一般用三种方式:
-
使用 JDK 提供的 jmap 工具,命令是 jmap -dump:format=b,file=<dumpfile> <pid>。当进程接近僵死时,可以添加 -F 参数强制转储:jmap -F -dump:format=b,file=<dumpfile> <pid>。
-
本地运行的 Java 进程,直接在 MAT 使用 File → accquire heap dump 功能获取。
-
启动 Java 进程时配置JVM参数:-XX:-HeapDumpOnOutOfMemoryError,当发生 OOM 时无需人工干预会自动生成 dump文件。指定目录用 -XX:HeapDumpPath=<filepath> 来设置。
1.2、一些小工具的说明
-
对象级别分析的 Path to GC Root(到GC Root的路径)
-
with outgoing references(当前对象引用了那些对象信息)
-
with incoming references (那些对象引用了当前对象)
二、全局信息
堆内存大小、类数量、实例数量、Class Loader数量。
-
全局概览信息,堆内存大小、类数量、实例数量、Class Loader数量。
-
Unreachable Object Histogram,展现转储快照时可被回收的对象信息(一般不需要关注,除非 GC 频繁影响实时性的场景分析才用到)
-
Biggest Objects by Retained Size,展现经过统计过的哪几个实例所关联的对象占内存总和较高,以及具体占用的内存大小,一般相关代码比较简单情况下,往往可以直接分析具体的引用关系异常,如内存泄漏等。此外也包含了最大对象和链接支持继续深入分析。
-
MAT 分析过的 Top Consumers (top消耗内存的信息)、Leak Suspects(内存泄漏嫌疑)等。
三、柱状图信息 Histogram
功能
-
罗列每个类实例的数量、类实例累计内存占比,包括自身内存占用量(Shallow Heap)及支配对象的内存占用量(Retain Heap)。
-
支持按对象数量、Retained Heap、Shallow Heap(默认排序)等指标排序;支持按正则过滤;支持按 package、class loader、super class、class 聚类统计,
使用入口:MAT 主界面 → Histogram;注意 Histogram 默认不展现 Retained Heap,可以使用计算器图标计算,如下图所示。
使用场景
-
有些情况 Dominator tree 无法展现出热点对象(上文提到 Dominator tree 支配内存排名前20的占比均不高,或者按 class 聚合也无明显热点对象,此时 Dominator tree 很难做关联分析判断哪类对象占比高),这时可以使用 Histogram 查看所有对象所属类的分布,快速定位占据 Retained Heap 大头的类。
-
使用技巧
1). Integer,String 和 Object[] 一般不直接导致内存问题。为更好的组织视图,可以通过 class loader 或 package 分组进一步聚焦,如下图。
2). Histogram 支持使用正则表达式来过滤。例如,我们可以只展示那些匹配com.q.*的类
3). 可以在 Histogram 的某个类继续使用 outgoing reference 查看对象分布,进而定位哪些对象是大头
四、支配树 Dominator Tree
功能
展现对象的支配关系图,并给出对象支配内存的大小(支配内存等同于 Retained Heap,即其被 GC 回收可释放的内存大小)
支持排序、支持按 package、class loader、super class、class 聚类统计
使用入口:全局支配树: MAT 主界面 → Dominator tree。
举例: 下图中通过查看 Dominator tree,了解到内存主要是由 ThreadAndListHolder-thread 及 main 两个线程支配(后面第2.6节会给出整体案例)。
使用场景
-
开始 Dump 分析时,首先应使用 Dominator tree 了解各支配树起点对象所支配内存的大小,进而了解哪几个起点对象是 GC 无法释放大内存的原因。
-
当个别对象支配树的 Retained Heap 很大存在明显倾斜时,可以重点分析占比高的对象支配关系,展开子树进一步定位到问题根因,如下图中可看出最终是 SameContentWrapperContainer 对象持有的 ArrayList 过大
-
在 Dominator tree 中展开树状图,可以查看支配关系路径(与 outgoing reference 的区别是:如果 X 支配 Y,则 X 释放后 Y必然可释放;如果仅仅是 X 引用 Y,可能仍有其他对象引用 Y,X 释放后 Y 仍不能释放,所以 Dominator tree 去除了 incoming reference 中大量的冗余信息
-
有些情况下可能并没有支配起点对象的 Retained Heap 占用很大内存(比如 class X 有100个对象,每个对象的 Retained Heap 是10M,则 class X 所有对象实际支配的内存是 1G,但可能 Dominator tree 的前20个都是其他class 的对象),这时可以按 class、package、class loader 做聚合,进而定位目标。
例如:
下图中各 GC Roots 所支配的内存均不大,这时需要聚合定位爆发点。 -
在 Dominator tree 展现后按 class 聚合,如下图
-
可以定位到是 SomeEntry 对象支配内存较多,然后结合代码进一步分析具体原因
-
在一些操作后定位到异常持有 Retained Heap 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下
五、Leak Suspects
功能:具备自动检测内存泄漏功能,罗列可能存在内存泄漏的问题点。
使用入口:一般当存在明显的内存泄漏时,分析完Dump文件后就会展现,也可以如下图在 MAT 主页 → Leak Suspects。
使用场景:需要查看引用链条上占用内存较多的可疑对象。这个功能可解决一些基础问题,但复杂的问题往往帮助有限。
举例
-
下图中 Leak Suspects 视图展现了两个线程支配了绝大部分内存。
-
下图是点击上图中 Keywords 中 “Details” ,获取实例到 GC Root 的最短路径、dominator 路径的细信息。
六、Top Consumers
-
功能:最大对象报告,可以展现哪些类、哪些 class loader、哪些 package 占用最高比例的内存,其功能 Histogram 及 Dominator tree 也都支持。
-
使用场景:应用程序发生内存泄漏时,查看哪些泄漏的对象通常在 Dump 快照中会占很大的比重。因此,对简单的问题具有较高的价值。
七、配置MAT不忽略要垃圾回收对象
查询老年代对象列表的OQL语句
SELECT * FROM INSTANCEOF java.lang.Object t WHERE (toHex(t.@objectAddress) >= {老年代堆内存起始地址} AND toHex(t.@objectAddress) <= {老年代堆内存结束地址})
内存地址的确认方法
依赖应用服务的GC日志,查询老年代的起始地址和结束地址 开启 GC 日志 java -XX:+PrintGCDetails -XX:+HeapDumpBeforeFullGC -XX:+PrintHeapAtGC 得如如下类似日志 [PSYoungGen: 611840K->992K(612352K)] 1260846K->650606K(1308672K), 0.0047655 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] Heap after GC invocations=30667 (full 91): PSYoungGen total 612352K, used 992K [0x00000000da800000, 0x0000000100000000, 0x0000000100000000) eden space 610816K, 0% used [0x00000000da800000,0x00000000da800000,0x00000000ffc80000) from space 1536K, 64% used [0x00000000ffc80000,0x00000000ffd78000,0x00000000ffe00000) to space 1536K, 0% used [0x00000000ffe80000,0x00000000ffe80000,0x0000000100000000) ParOldGen total 696320K, used 649614K [0x00000000b0000000, 0x00000000da800000, 0x00000000da800000) object space 696320K, 93% used [0x00000000b0000000,0x00000000d7a63bf8,0x00000000da800000) Metaspace used 69066K, capacity 70738K, committed 71936K, reserved 1114112K class space used 7654K, capacity 8001K, committed 8192K, reserved 1048576K } 可以看到 ParOldGen 对象的内存地址为 0x00000000b0000000,0x00000000d7a63bf8,0x00000000da800000 0x00000000b0000000 表示是 OldGen 的起始地址 0x00000000d7a63bf8 表示是使用到的地址. b0000000 ~ d7a63bf8 0x00000000da800000 表示 OldGen 的结束地址