本系列共三篇文章, 本文是系列第2篇——进阶篇,详细讲解 MAT 各种工具的核心功能、用法、适用场景,并在具体实战场景下讲解帮大家学习如何针对各类内存问题。
《JVM 内存分析工具 MAT 的深度讲解与实践——入门篇》 介绍 MAT 产品功能、基础概念、与其他工具对比、Quick Start 指南。
《JVM 内存分析工具 MAT 的深度讲解与实践——进阶篇》 展开并详细讲解 MAT 各种工具的核心功能、用法、场景,并在具体实战场景下讲解帮大家加深体会。
《JVM 内存分析工具 MAT 的深度讲解与实践——高阶篇》 总结复杂内存问题的系统性分析方法,并通过一个综合案例提升大家的实战能力。
一、前言
熟练掌握 MAT 是 Java 高手的必备能力,但实践时大家往往需面对众多功能,眼花缭乱不知如何下手,小编也没有找到一篇完善的教学素材,所以整理本文帮大家系统掌握 MAT 分析工具。
注:在该系列开篇文章《JVM 内存分析工具 MAT 的深度讲解与实践——入门篇》中介绍了 MAT 的使用场景及安装方法,不熟悉 MAT 的读者建议先阅读上文并安装,本文案例很容易在本地实践。
二、内存分布详解及实战
2.1 内存分布详解及实战
方法区溢出时(Java 8后不使用方法区,对应堆溢出),查看 class 数量异常多,可以考虑是否为动态代理类异常载入过多或类被反复重复加载。
方法区溢出时,查看 class loader 数量过多,可以考虑是否为自定义 class loader 被异常循环使用。
GC Root 过多,可以查看 GC Root 分布,理论上这种情况极少会遇到,笔者只在 JNI 使用一个存在 BUG 的库时遇到过。
线程数过多,一般是频繁创建线程但无法执行结束,从概览可以了解异常表象,具体原因可以参考本文线程分析部分内容,此处不展开。
2.2 Dominator tree
功能
展现对象的支配关系图,并给出对象支配内存的大小(支配内存等同于 Retained Heap,即其被 GC 回收可释放的内存大小)
支持排序、支持按 package、class loader、super class、class 聚类统计
使用场景
在 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 所支配的内存均不大,这时需要聚合定位爆发点。
在一些操作后定位到异常持有 Retained Heap 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下。
2.3 Histogram 直方图
功能
使用场景
使用技巧
Integer,String 和 Object[] 一般不直接导致内存问题。为更好的组织视图,可以通过 class loader 或 package 分组进一步聚焦,如下图。
Histogram 支持使用正则表达式来过滤。例如,我们可以只展示那些匹配com.q.*的类。
可以在 Histogram 的某个类继续使用 outgoing reference 查看对象分布,进而定位哪些对象是大头。
2.4 Leak Suspects
下图中 Leak Suspects 视图展现了两个线程支配了绝大部分内存。
下图是点击上图中 Keywords 中 "Details" ,获取实例到 GC Root 的最短路径、dominator 路径的细信息。
2.5 Top Consumers
2.6 综合案例一
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;
}
}
引用关系图
先来看方向一,在 Heap Dump Overview 中可以快速定位到 Number of class loaders 数达50万以上,这种基本属于异常情况,如下图所示。
使用 incoming references,可以找到创建的代码位置。
查看不同线程持有的内存占比,定位高内存消耗线程(开发技巧:不要直接使用 Thread 或 Executor 默认线程名避免全部混合在一起,使用线程尽量自命名方便识别,如下图中 ThreadAndListHolder-thread 是自定义线程名,可以很容易定位到具体代码)
查看线程的执行栈及变量,结合业务代码了解线程阻塞在什么地方,以及无法继续运行释放内存,如下图中 ThreadAndListHolder-thread 阻塞在 sleep 方法。
技巧:在排查内存泄漏时,建议 exclude all phantom/weak/soft etc.references 排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以被 GC 给回收,聚焦是否还存在 Strong 引用链即可。
功能
当从 Heap dump overview 了解到系统中 class loader 过多,导致占用内存异常时进入更细致的分析定位根因时使用。
解决 NoClassDefFoundError 问题或检测 jar 包是否被重复加载
进入 MAT 已加载的重复类检测功能,方式如下图。
可以看到所有重复的类,以及相关的类加载器,如下图。
4. 对象状态详解及实战
4.1 inspector
当内存使用量与业务逻辑有较强关联的场景,通过 inspector 可以通过查看对象具体属性值。比如:社交场景中某个用户对象的好友列表异常,其 List 长度达到几亿,通过 inspector 面板获取到异常用户 ID,进而从业务视角继续排查属于哪个用户,本例可能有系统账号,与所有用户是好友。
集合等类型的使用会较多,如查看 ArrayList 的 size 属性也就了解其大小。
4.2 集合状态
使用场景
通过对 ArrayList 或数组等集合类对象按填充率聚类,定位稀疏或空集合类对象造成的内存浪费。
通过 HashMap 冲突率判定 hash 策略是否合理。
4.3 综合案例三
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.001 * LIST_SIZE) {
list1.add("" + i);
list2.add("" + i);
}
if (i < 0.05 * 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
SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression> ]
Select 子句可以使用“*”,查看结果对象的引用实例(相当于 outgoing references);可以指定具体的内容,如 Select OBJECTS v.elementData from xx 是返回的结果是完整的对象,而不是简单的对象描述信息);可以使用 Distinct 关键词去重。
From 指定查询范围,一般指定类名、正则表达式、对象地址。
Where 用来指定筛选条件。
全部语法详见:https://help.eclipse.org/2020-12/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Freference%2Foqlsyntax.html
未支持的核心功能:group by value,如果有需求可以先导出结果到 csv 中,再使用 awk 等脚本工具分析即可。
例子:查找 size=0 且未使用过的 ArrayList
select * from java.util.ArrayList where size=0 and modCount=0。
5.2 检索及筛选
5.3 按地址寻址
5.4 综合案例四
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());
}
}
}
分析过程
至此本文讲解了 MAT 各项工具的功能、使用方法、适用场景,也穿插了4个实战案例,熟练掌握对分析 JVM 内存问题大有裨益,尤其是各种功能的组合使用。在下一篇《JVM 内存分析工具 MAT 的深度讲解与实践——高阶篇》会总结 JVM 堆内存分析的系统性方法,并在更复杂的案例中实践。
参考内容
MAT官网: https://help.eclipse.org/2020-09/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html
10 Tips for using the Eclipse Memory Analyzer: https://eclipsesource.com/blogs/2013/01/21/10-tips-for-using-the-eclipse-memory-analyzer/
Finding Memory Leaks with SAP Memory Analyzer: https://blogs.sap.com/2007/07/02/finding-memory-leaks-with-sap-memory-analyzer/
An effective way to fight duplicated libs and version conflicting classes using Memory Analyzer Tool: https://community.bonitasoft.com/blog/effective-way-fight-duplicated-libs-and-version-conflicting-classes-using-memory-analyzer-tool
Memory for nothing(Empty collection problem): https://blogs.sap.com/2007/08/02/memory-for-nothing/