本文详细讲解 MAT 众多内存分析工具功能,这些功能组合使用异常强大,熟练使用几乎可以解决所有的堆内存离线分析的问题。我们将功能划分为4类:内存分布详情、对象间依赖、对象状态详情、按条件检索。每大类有多个功能点,本文会逐一讲解各功能的场景及用法。此外,添加了原创或引用案例加强理解和掌握。
注:在该系列开篇文章《JVM 内存分析工具 MAT 的深度讲解与实践——入门篇》中介绍了 MAT 的使用场景及安装方法,不熟悉 MAT 的读者建议先阅读上文并安装,本文案例很容易在本地实践。另外,上文中产品介绍部分顺序也对应本文功能讲解的组织,如下图:
为减少对眼花缭乱的菜单的迷茫,可以通过下图先整体熟悉下各功能使用入口,后续都会讲到
功能:展现堆内存大小、对象数量、class 数量、class loader 数量、GC Root 数量、环境变量、线程概况等全局统计信息。
使用入口:MAT 主界面 → Heap Dump Overview
举例:下面是对象数量、class loader 数量、GC Root 数量,可以看出 class loader 存在异常
举例:下图是线程概况,可以查看每个线程名、线程的 Retained Heap、daemon 属性等
使用场景 全局概览呈现全局统计信息,重点查看整体是否有异常数据,所以有效信息有限,下面几种场景有一定帮助:
注:笔者使用频率的 Top1,是高效分析 Dump 必看的功能。
功能
展现对象的支配关系图,并给出对象支配内存的大小(支配内存等同于 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 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下
注:笔者使用频率 Top2
功能
使用入口:MAT 主界面 → Histogram;注意 Histogram 默认不展现 Retained Heap,可以使用计算器图标计算,如下图所示。
使用场景
功能:具备自动检测内存泄漏功能,罗列可能存在内存泄漏的问题点。
使用入口:一般当存在明显的内存泄漏时,分析完Dump文件后就会展现,也可以如下图在 MAT 主页 → Leak Suspects。
使用场景:需要查看引用链条上占用内存较多的可疑对象。这个功能可解决一些基础问题,但复杂的问题往往帮助有限。
举例
使用工具项: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, "" , "()V", null, null);
//生成构造方法的字节码指令
mw.visitVarInsn(ALOAD, 0);
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "" , "()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
分析过程
1 . 首先进入 Dominator tree,可以看出是 SameContentWrapperContainerProxy 对象与 main 线程两者持有99%内存不能释放导致 OOM。
2. 先来看方向一,在 Heap Dump Overview 中可以快速定位到 Number of class loaders 数达50万以上,这种基本属于异常情况,如下图所示。
3.使用 Class Loader Explorer 分析工具,此时会展现类加载详情,可以看到有524061个 class loader。我们的案例中仅有ClassLoaderOOMOps 这样的自定义类加载器,所以很快可以定位到问题
4.如果类加载器较多,不能确定是哪个引发问题,则可以将所有的 class loader对象按类做聚类,如下图所示。
5.Histogram 会根据 class 聚合,并展现对象数量级其 Shallow Heap 及 Retained Heap(如Retained Heap项目为空,可以点击下图中计算机的图标并计算 Retained Heap),可以看到 ClassLoaderOOMOps 有524044个对象,其 Retain Heap 占据了370M以上(上述代码是100M左右
6.使用 incoming references,可以找到创建的代码位置
7.再来看方向二,同样在占据319M内存的 Obejct 数组采用 incoming references 查看引用路径,也很容易定位到具体代码位置。并且从下图中我们看出,Dominator tree 的起点并不一定是 GC根,且通过 Dominator tree 可能无法获取到最开始的创建路径,但 incoming references 是可以的
注:笔者使用频率 Top2
功能:在对象引用图中查看某个特定对象的所有引用关系(提供对象对其他对象或基本类型的引用关系,以及被外部其他对象的引用关系)。通过任一对象的直接引用及间接引用详情(主要是属性值及内存占用),提供完善的依赖链路详情。
使用入口:目标域右键 → List objects → with outgoing references/with incoming references.
使用场景
功能:展现转储 dump 文件时线程执行栈、线程栈引用的对象等详细状态,也提供各线程的 Retained Heap 等关联内存信息。
使用入口:MAT 主页 → Thread overview
使用场景
功能:提供任一对象到 GC Root 的路径详情。
使用入口:目标域右键 → Path To GC Roots
使用场景:有时你确信已经处理了大的对象集合但依然无法回收,该功能能快速定位异常对象不能被 GC 回收的原因,直击异常对象到 GC Root 的引用路径。比 incoming reference 的优势是屏蔽掉很多不需关注的引用关系,比 Dominator tree 的优势是可以得到更全面的信息。
小技巧:在排查内存泄漏时,建议选择 exclude all phantom/weak/soft etc.references 排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被 GC 给回收,聚焦在对象否还存在 Strong
引用链即可。
功能
使用场景
具体使用方法在 2.6 及 3.5 两节的案例中有介绍。
使用工具项:class loader(重复类检测)、inspector、正则检索。
异常现象 :运行时报 NoClassDefFoundError,在 classpath 中有两个不同版本的同名类。
分析过程
功能:MAT 通过 inspector 面板展现对象的详情信息,如静态属性值及实例属性值、内存地址、类继承关系、package、class loader、GC Roots 等详情数据。
使用场景
举例:下图中左边的 Inspector 窗口展现了地址 0x125754cf8 的 ArrayList 实例详情,包括 modCount 等并不会在 outgoing references 展现的基本属性
功能:帮助更直观的了解系统的内存使用情况,查找浪费的内存空间。
使用入口:MAT 主页 → Java Collections → 填充率/Hash冲突等功能。
使用场景
使用工具项: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;
}
}
分析过程
功能:提供一种类似于SQL的对象(类)级别统一结构化查询语言,根据条件对堆中对象进行筛选。
语法
SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression> ]
使用场景
功能:本文第二章内存分布,第三章对象间依赖的众多功能,均支持按字符串检索、按正则检索等操作。
使用场景:在使用 Histogram、Thread overview 等功能时,可以进一步添加字符串匹配、正则匹配条件过滤缩小排查范围。
功能:根据对象的虚拟内存十六进制地址查找对象。
使用场景:仅知道地址并希望快速查看对象做后续分析时使用,其余可以直接使用 outgoing reference 了解对象信息。
使用工具项: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 内存问题大有裨益,尤其是各种功能的组合使用。在下一篇《JVM 内存分析工具 MAT 的深度讲解与实践——高阶篇》会总结 JVM 堆内存分析的系统性方法,并在更复杂的案例中实践。
参考内容
Q的博客