一文深度讲解JVM 内存分析工具 MAT及实践(建议收藏)
1. 前言
熟练掌握 MAT 是 Java 高手的必备能力,但实践时大家往往需面对众多功能,眼花缭乱不知如何下手,小编也没有找到一篇完善的教学素材,所以整理本文帮大家系统掌握 MAT 分析工具。
本文详细讲解 MAT 众多内存分析工具功能,这些功能组合使用异常强大,熟练使用几乎可以解决所有的堆内存离线分析的问题。我们将功能划分为4类:内存分布详情、对象间依赖、对象状态详情、按条件检索。每大类有多个功能点,本文会逐一讲解各功能的场景及用法。此外,添加了原创或引用案例加强理解和掌握。
如图所示:
为减少对眼花缭乱的菜单的迷茫,可以通过下图先整体熟悉下各功能使用入口,后续都会讲到。
2. 内存分布详解及实战
2.1 全局信息概览
功能:展现堆内存大小、对象数量、class 数量、class loader 数量、GC Root 数量、环境变量、线程概况等全局统计信息。
使用入口:MAT 主界面 → Heap Dump Overview。
举例:下面是对象数量、class loader 数量、GC Root 数量,可以看出 class loader 存在异常。
举例:下图是线程概况,可以查看每个线程名、线程的 Retained Heap、daemon 属性等。
使用场景 全局概览呈现全局统计信息,重点查看整体是否有异常数据,所以有效信息有限,下面几种场景有一定帮助:
2.2 Dominator tree
注:笔者使用频率的 Top1,是高效分析 Dump 必看的功能。
功能
使用入口:全局支配树: MAT 主界面 → Dominator tree。
举例: 下图中通过查看 Dominator tree,了解到内存主要是由 ThreadAndListHolder-thread 及 main 两个线程支配(后面第2.6节会给出整体案例)。
使用场景
在 Dominator tree 展现后按 class 聚合,如下图:
可以定位到是 SomeEntry 对象支配内存较多,然后结合代码进一步分析具体原因。
在一些操作后定位到异常持有 Retained Heap 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下。
2.3 Histogram 直方图
注:笔者使用频率 Top2
功能
使用入口:MAT 主界面 → Histogram;注意 Histogram 默认不展现 Retained Heap,可以使用计算器图标计算,如下图所示。
使用场景
使用技巧
Histogram 支持使用正则表达式来过滤。例如,我们可以只展示那些匹配com.q.*的类。
可以在 Histogram 的某个类继续使用 outgoing reference 查看对象分布,进而定位哪些对象是大头
2.4 Leak Suspects
功能:具备自动检测内存泄漏功能,罗列可能存在内存泄漏的问题点。
使用入口:一般当存在明显的内存泄漏时,分析完Dump文件后就会展现,也可以如下图在 MAT 主页 → Leak Suspects。
使用场景:需要查看引用链条上占用内存较多的可疑对象。这个功能可解决一些基础问题,但复杂的问题往往帮助有限。
举例
下图是点击上图中 Keywords 中 "Details" ,获取实例到 GC Root 的最短路径、dominator 路径的细信息。
2.5 Top Consumers
功能:最大对象报告,可以展现哪些类、哪些 class loader、哪些 package 占用最高比例的内存,其功能 Histogram 及 Dominator tree 也都支持。
使用场景:应用程序发生内存泄漏时,查看哪些泄漏的对象通常在 Dump 快照中会占很大的比重。因此,对简单的问题具有较高的价值。
2.6 综合案例一
使用工具项:Heap dump overview、Dominator tree、Histogram、Class Loader Explorer(见3.4节)、incoming references(见3.1节)
程序代码
package com.q.mat;
import java.util.*;
import org.objectweb.asm.*;
public class ClassLoaderOOMOps extends ClassLoader implements Opcodes {
public static void main(final String args[]) throws Exception {
new ThreadAndListHolder(); // ThreadAndListHolder 类中会加载大对象
List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
final String className = "ClassLoaderOOMExample";
final byte[] code = geneDynamicClassBytes(className);
// 循环创建自定义 class loader,并加载 ClassLoaderOOMExample
while (true) {
ClassLoaderOOMOps loader = new ClassLoaderOOMOps();
Class<?> exampleClass = loader.defineClass(className, code, 0, code.length); //将二进制流加载到内存中
classLoaders.add(loader);
// exampleClass.getMethods()[0].invoke(null, new Object[]{null}); // 执行自动加载类的方法,通过反射调用main
}
}
private static byte[] geneDynamicClassBytes(String className) throws Exception {
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_1, ACC_PUBLIC, className, null, "java/lang/Object", null);
//生成默认构造方法
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
//生成构造方法的字节码指令
mw.visitVarInsn(ALOAD, 0);
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mw.visitInsn(RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
//生成main方法
mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
//生成main方法中的字节码指令
mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mw.visitLdcInsn("Hello world!");
mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
mw.visitInsn(RETURN);
mw.visitMaxs(2, 2);
mw.visitEnd(); //字节码生成完成
return cw.toByteArray(); // 获取生成的class文件对应的二进制流
}
}
package com.q.mat;
import java.util.*;
import org.objectweb.asm.*;
public class ThreadAndListHolder extends ClassLoader implements Opcodes {
private static Thread innerThread1;
private static Thread innerThread2;
private static final SameContentWrapperContainerProxy sameContentWrapperContainerProxy = new SameContentWrapperContainerProxy();
static {
// 启用两个线程作为 GC Roots
innerThread1 = new Thread(new Runnable() {
public void run() {
SameContentWrapperContainerProxy proxy = sameContentWrapperContainerProxy;
try {
Thread.sleep(60 * 60 * 1000);
} catch (Exception e) {
System.exit(1);
}
}
});
innerThread1.setName("ThreadAndListHolder-thread-1");
innerThread1.start();
innerThread2 = new Thread(new Runnable() {
public void run() {
SameContentWrapperContainerProxy proxy = proxy = sameContentWrapperContainerProxy;
try {
Thread.sleep(60 * 60 * 1000);
} catch (Exception e) {
System.exit(1);
}
}
});
innerThread2.setName("ThreadAndListHolder-thread-2");
innerThread2.start();
}
}
class IntArrayListWrapper {
private ArrayList<Integer> list;
private String name;
public IntArrayListWrapper(ArrayList<Integer> list, String name) {
this.list = list;
this.name = name;
}
}
class SameContentWrapperContainer {
// 2个Wrapper内部指向同一个 ArrayList,方便学习 Dominator tree
IntArrayListWrapper intArrayListWrapper1;
IntArrayListWrapper intArrayListWrapper2;
public void init() {
// 线程直接支配 arrayList,两个 IntArrayListWrapper 均不支配 arrayList,只能线程运行完回收
ArrayList<Integer> arrayList = generateSeqIntList(10 * 1000 * 1000, 0);
intArrayListWrapper1 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-1");
intArrayListWrapper2 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-2");
}
private static ArrayList<Integer> generateSeqIntList(int size, int startValue) {
ArrayList<Integer> list = new ArrayList<Integer>(size);
for (int i = startValue; i < startValue + size; i++) {
list.add(i);
}
return list;
}
}
class SameContentWrapperContainerProxy {
SameContentWrapperContainer sameContentWrapperContainer;
public SameContentWrapperContainerProxy() {
SameContentWrapperContainer container = new SameContentWrapperContainer();
container.init();
sameContentWrapperContainer = container;
}
}
启动参数:-Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/gjd/Desktop/dump/heapdump.hprof
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
引用关系图
分析过程
- 首先进入 Dominator tree,可以看出是 SameContentWrapperContainerProxy 对象与 main 线程两者持有99%内存不能释放导致 OOM。
先来看方向一,在 Heap Dump Overview中可以快速定位到 Number of class loaders 数达50万以上,这种基本属于异常情况,如下图所示。
使用 Class Loader Explorer 分析工具,此时会展现类加载详情,可以看到有524061个 class loader。我们的案例中仅有ClassLoaderOOMOps 这样的自定义类加载器,所以很快可以定位到问题。
如果类加载器较多,不能确定是哪个引发问题,则可以将所有的 class loader对象按类做聚类,如下图所示。
Histogram 会根据 class 聚合,并展现对象数量及其 Shallow Heap 及 Retained Heap(如Retained Heap项目为空,可以点击下图中计算机的图标并计算 Retained Heap),可以看到 ClassLoaderOOMOps 有524044个对象,其 Retain Heap 占据了370M以上(上述代码是100M左右)。
使用 incoming references,可以找到创建的代码位置。
再来看方向二,同样在占据319M内存的 Obejct 数组采用 incoming references 查看引用路径,也很容易定位到具体代码位置。并且从下图中我们看出,Dominator tree 的起点并不一定是 GC根,且通过 Dominator tree 可能无法获取到最开始的创建路径,但 incoming references 是可以的。
3. 对象间依赖详解及实战
3.1 References
注:笔者使用频率 Top2
功能:在对象引用图中查看某个特定对象的所有引用关系(提供对象对其他对象或基本类型的引用关系,以及被外部其他对象的引用关系)。通过任一对象的直接引用及间接引用详情(主要是属性值及内存占用),提供完善的依赖链路详情。
使用入口:目标域右键 → List objects → with outgoing references/with incoming references.
使用场景
3.2 Thread overview
功能:展现转储 dump 文件是线程执行栈、线程栈引用的对象等详细状态,也提供各线程的 Retained Heap 等关联内存信息。
使用入口:MAT 主页 → Thread overview
使用场景
3.3 Path To GC Roots
功能:提供任一对象到 GC Root 的路径详情。
使用入口:目标域右键 → Path To GC Roots
使用场景:有时你确信已经处理了大的对象集合但依然无法回收,该功能能快速定位异常对象不能被 GC 回收的原因,直击异常对象到 GC Root 的引用路径。比 incoming reference 的优势是屏蔽掉很多不需关注的引用关系,比 Dominator tree 的优势是可以得到更全面的信息。
小技巧:在排查内存泄漏时,建议选择 exclude all phantom/weak/soft etc.references 排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被 GC 给回收,聚焦在对象是否还存在 Strong 引用链即可。
3.4 class loader 分析
功能
使用场景
具体使用方法在 2.6 及 3.5 两节的案例中有介绍。
3.5 综合案例二
使用工具项:class loader(重复类检测)、inspector、正则检索。
异常现象 :运行时报 NoClassDefFoundError,在 classpath 中有两个不同版本的同名类。
分析过程
- 进入 MAT 已加载的重复类检测功能,方式如下图。
可以看到所有重复的类,以及相关的类加载器,如下图。
4. 对象状态详解及实战
4.1 inspector
功能:MAT 通过 inspector 面板展现对象的详情信息,如静态属性值及实例属性值、内存地址、类继承关系、package、class loader、GC Roots 等详情数据。
使用场景
举例:下图中左边的 Inspector 窗口展现了地址 0x125754cf8 的 ArrayList 实例详情,包括 modCount 等并不会在 outgoing references 展现的基本属性。
4.2 集合状态
功能:帮助更直观的了解系统的内存使用情况,查找浪费的内存空间。
使用入口:MAT 主页 → Java Collections → 填充率/Hash冲突等功能。
使用场景
具体使用方法在 4.3 节案例详细介绍。
4.3 综合案例三
使用工具项:Dominator tree、Histogram、集合 ratio。
异常现象 :程序 OOM,且 Dominator tree 无大对象,通过 Histogram 了解到多个 ArrayList 占据大量内存,期望通过减少 ArrayList 优化程序。
程序代码
package com.q.mat;
import java.util.ArrayList;
import java.util.List;
public class ListRatioDemo {
public static void main(String[] args) {
for(int i=0;i<10000;i++){
Thread thread = new Thread(new Runnable() {
public void run() {
HolderContainer holderContainer1 = new HolderContainer();
try {
Thread.sleep(1000 * 1000 * 60);
} catch (Exception e) {
System.exit(1);
}
}
});
thread.setName("inner-thread-" + i);
thread.start();
}
}
}
class HolderContainer {
ListHolder listHolder1 = new ListHolder().init();
ListHolder listHolder2 = new ListHolder().init();
}
class ListHolder {
static final int LIST_SIZE = 100 * 1000;
List<String> list1 = new ArrayList(LIST_SIZE); // 5%填充
List<String> list2 = new ArrayList(LIST_SIZE); // 5%填充
List<String> list3 = new ArrayList(LIST_SIZE); // 15%填充
List<String> list4 = new ArrayList(LIST_SIZE); // 30%填充
public ListHolder init() {
for (int i = 0; i < LIST_SIZE; i++) {
if (i < 0.05 * LIST_SIZE) {
list1.add("" + i);
list2.add("" + i);
}
if (i < 0.15 * LIST_SIZE) {
list3.add("" + i);
}
if (i < 0.3 * LIST_SIZE) {
list4.add("" + i);
}
}
return this;
}
}
分析过程
- 使用 Dominator tree 查看并无高占比起点。
使用 Histogram 定位到 ListHolder 及 ArrayList 占比过高,经过业务分析很多 List 填充率很低,不会浪费内存。
查看 ArrayList 的填充率,MAT 首页 → Java Collections → Collection Fill Ratio。
查看类型填写 java.util.ArrayList。
从结果可以看出绝大部分 ArrayList 初始申请长度过大。
5. 按条件检索详解及实战
5.1 OQL
功能:提供一种类似于SQL的对象(类)级别统一结构化查询语言,根据条件对堆中对象进行筛选。
语法
SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression> ]
例子:查找 size=0 且未使用过的 ArrayList:select * from java.util.ArrayList where size=0 and modCount=0。
使用场景
例如:微服务的分布式链路追踪系统,采集各服务所有接口名,共计200个服务却采集到了200万个接口名(一个服务不会有1万个接口),这时直接在 List 中一个个查看很难定位,可以直接用 OQL 导出,定位哪个服务接口名收集异常(如把 URL 中 ID 也统计到接口中了)
5.2 检索及筛选
功能:本文第二章内存分布,第三章对象间依赖的众多功能,均支持按字符串检索、按正则检索等操作。
使用场景:在使用 Histogram、Thread overview 等功能时,可以进一步添加字符串匹配、正则匹配条件过滤缩小排查范围。
5.3 按地址寻址
功能:根据对象的虚拟内存十六进制地址查找对象。
使用场景:仅知道地址并希望快速查看对象做后续分析时使用,其余可以直接使用 outgoing reference 了解对象信息。
5.4 综合案例四
使用工具项:OQL、Histogram、incoming references
异常现象及目的 :程序占用内存高,存在默认初始化较长的 ArrayList,需分析 ArrayList 被使用的占比,通过数据支撑是否采用懒加载模式,并分析具体哪块代码创建了空 ArrayList。
程序代码
public class EmptyListDemo {
public static void main(String[] args) {
EmptyValueContainerList emptyValueContainerList = new EmptyValueContainerList();
FilledValueContainerList filledValueContainerList = new FilledValueContainerList();
System.out.println("start sleep...");
try {
Thread.sleep(50 * 1000 * 1000);
} catch (Exception e) {
System.exit(1);
}
}
}
class EmptyValueContainer {
List<Integer> value1 = new ArrayList(10);
List<Integer> value2 = new ArrayList(10);
List<Integer> value3 = new ArrayList(10);
}
class EmptyValueContainerList {
List<EmptyValueContainer> list = new ArrayList(500 * 1000);
public EmptyValueContainerList() {
for (int i = 0; i < 500 * 1000; i++) {
list.add(new EmptyValueContainer());
}
}
}
class FilledValueContainer {
List<Integer> value1 = new ArrayList(10);
List<Integer> value2 = new ArrayList(10);
List<Integer> value3 = new ArrayList(10);
public FilledValueContainer init() {
value1.addAll(Arrays.asList(1, 3, 5, 7, 9));
value2.addAll(Arrays.asList(2, 4, 6, 8, 10));
value1.addAll(Arrays.asList(1, 1, 1, 1, 1, 1, 1, 1, 1, 1));
return this;
}
}
class FilledValueContainerList {
List<FilledValueContainer> list = new ArrayList(500);
public FilledValueContainerList() {
for (int i = 0; i < 500; i++) {
list.add(new FilledValueContainer().init());
}
}
}
分析过程
- 内存中有50万个 capacity = 10 的空 ArrayList 实例。我们分析下这些对象的占用内存总大小及对象创建位置,以便分析延迟初始化(即直到使用这些对象的时候才将之实例化,否则一直为null)是否有必要。
- 使用 OQL 查询出初始化后未被使用的 ArrayList(size=0 且 modCount=0),语句如下图。可以看出公有 150 万个空 ArrayList,这些对象属于浪费内存。我们接下来计算下总计占用多少内存,并根据结果看是否需要优化。
计算 150万 ArrayList占内存总量,直接点击右上方带黄色箭头的 Histogram 图标,这个图标是在选定的结果再用直方图展示,总计支配了120M 左右内存(所以这里点击结果,不包含 modCount 或 size 大于0的 ArrayList 对象)。这类在选定结果继续分析很多功能都支持,如正则检索、Histogram、Dominator tree等等。
查看下图 ArrayList 的具体来源,可用 incoming references,下图中显示了清晰的对象创建路径。
总结
至此本文讲解了 MAT 各项工具的功能、使用方法、适用场景,也穿插了4个实战案例,熟练掌握对分析 JVM 内存问题大有裨益,尤其是各种功能的组合使用。
作者:Q的博客
来源:https://juejin.cn/post/6911624328472133646