注:本文原创,转发需标明作者及原文链接。欢迎关注 【0广告微信公众号:Q的博客】。
本系列共三篇文章, 本文是系列第2篇——进阶篇,详细讲解 MAT 各种工具的 核心功能、用法、适用场景,并在具体实战场景下讲解帮大家学习如何针对各类内存问题。
熟练掌握 MAT 是 Java 高手的必备能力,但实践时大家往往需面对众多功能,眼花缭乱不知如何下手,小编也没有找到一篇完善的教学素材,所以整理本文帮大家系统掌握 MAT 分析工具。
本文详细讲解 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 必看的功能。
功能
使用入口:全局支配树: 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 做聚合,进而定位目标。
在一些操作后定位到异常持有 Retained Heap 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下。
注:笔者使用频率 Top2
功能
使用入口:MAT 主界面 → Histogram;注意 Histogram 默认不展现 Retained Heap,可以使用计算器图标计算,如下图所示。
使用场景
功能:具备自动检测内存泄漏功能,罗列可能存在内存泄漏的问题点。
使用入口:一般当存在明显的内存泄漏时,分析完Dump文件后就会展现,也可以如下图在 MAT 主页 → Leak Suspects。
使用场景:需要查看引用链条上占用内存较多的可疑对象。这个功能可解决一些基础问题,但复杂的问题往往帮助有限。
举例
下图中 Leak Suspects 视图展现了两个线程支配了绝大部分内存。
下图是点击上图中 Keywords 中 "Details" ,获取实例到 GC Root 的最短路径、dominator 路径的细信息。
功能:最大对象报告,可以展现哪些类、哪些 class loader、哪些 package 占用最高比例的内存,其功能 Histogram 及 Dominator tree 也都支持。
使用场景:应用程序发生内存泄漏时,查看哪些泄漏的对象通常在 Dump 快照中会占很大的比重。因此,对简单的问题具有较高的价值。
使用工具项: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 classLoaders = new ArrayList();
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 list;
private String name;
public IntArrayListWrapper(ArrayList 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 arrayList = generateSeqIntList(10 * 1000 * 1000, 0);
intArrayListWrapper1 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-1");
intArrayListWrapper2 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-2");
}
private static ArrayList generateSeqIntList(int size, int startValue) {
ArrayList list = new ArrayList(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
复制代码
分析过程
注:笔者使用频率 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 中有两个不同版本的同名类。
分析过程
根据类名,在 框中输入类名可以过滤无效信息。
选中目标类,通过Inspector视图,可以看到被加载的类具体是在哪个jar包里。(本例中重复的类是被 URLClassloader 加载的,右键点击 “_context” 属性,最后点击 “Go Into”,在弹出的窗口中的属性 “_war” 值是被加载类的具体包位置)
功能:MAT 通过 inspector 面板展现对象的详情信息,如静态属性值及实例属性值、内存地址、类继承关系、package、class loader、GC Roots 等详情数据。
使用场景
举例:下图中左边的 Inspector 窗口展现了地址 0x125754cf8 的 ArrayList 实例详情,包括 modCount 等并不会在 outgoing references 展现的基本属性。
功能:帮助更直观的了解系统的内存使用情况,查找浪费的内存空间。
使用入口:MAT 主页 → Java Collections → 填充率/Hash冲突等功能。
使用场景
具体使用方法在 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 list1 = new ArrayList(LIST_SIZE); // 5%填充
List list2 = new ArrayList(LIST_SIZE); // 5%填充
List list3 = new ArrayList(LIST_SIZE); // 15%填充
List 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 ] [ WHERE ]
复制代码
使用场景
功能:本文第二章内存分布,第三章对象间依赖的众多功能,均支持按字符串检索、按正则检索等操作。
使用场景:在使用 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 value1 = new ArrayList(10);
List value2 = new ArrayList(10);
List value3 = new ArrayList(10);
}
class EmptyValueContainerList {
List list = new ArrayList(500 * 1000);
public EmptyValueContainerList() {
for (int i = 0; i < 500 * 1000; i++) {
list.add(new EmptyValueContainer());
}
}
}
class FilledValueContainer {
List value1 = new ArrayList(10);
List value2 = new ArrayList(10);
List 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 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的博客。 不定期发送干货,实践经验、系统总结、源码解读、技术原理。