MAT工具简介

MAT(全名:Memory Analyzer Tool),是一款快速便捷且功能强大丰富的 JVM 堆内存离线分析工具。其通过展现 JVM 异常时所记录的运行时堆转储快照(Heap dump)状态(正常运行时也可以做堆转储分析),帮助定位内存泄漏问题或优化大内存消耗逻辑。

一般来说,线上出现GC问题会有异常告警,此时应该做的是下线一个节点,导出dump文件保留事故现场,然后对线上服务做回滚/灰度处理,或者调整堆内存大小,或者重启服务器/手动触发FullGC快速回收内存,先保证服务正常运行,然后再用1天的时间对dump文件进行分析,排查出异常问题并进行修复。

基础概念

Heap Dump

Heap Dump 是 Java 进程堆内存在一个时间点的快照,其主要结构包含:

  • 所有对象的实例信息:对象所属类名、基础类型和引用类型的属性等
  • 所有类信息:类加载器、类名、继承关系、静态属性
  • GC Root:GC Root 代表通过可达性分析来判定 JVM 对象是否存活的起始集合。JVM 采用追踪式垃圾回收(Tracing GC)模式,从所有 GC Roots 出发通过引用关系可以关联的对象就是存活的(且不可回收),其余的不可达的对象(Unreachable object:如果无法从 GC Root 找到一条引用路径能到达某对象,则该对象为Unreachable object)可以回收。
  • 线程栈及局部变量:快照生成时刻的所有线程的线程栈帧,以及每个线程栈的局部变量

Shallow Heap

Shallow Heap 代表一个对象结构自身所占用的内存大小,不包括其属性引用对象所占的内存。

Retained Heap

Retained Heap 是一个对象被 GC 回收后,可释放的内存大小。如果一个对象A同时被两个对象B和C引用,那么B回收后不能释放掉A,因为A也被C引用了。

Dominator tree

如果所有指向对象 Y 的路径都经过对象 X,则 X 支配(dominate) Y

QQL

OQL 是类似于 SQL 的 MAT 专用统一查询语言,可以根据复杂的查询条件对 dump 文件中的类或者对象等数据进行查询筛选。

references

outgoing references、incoming references 可以直击对象间依赖关系,MAT 也提供了链式快速操作。

  • outgoing references:对象引用的外部对象(注意不包含对象的基本类型属性。基本属性内容可在 inspector 查看)。
  • incoming references:直接引用了当前对象的对象,每个对象的 incoming references 可能有 0 到多个。

MAT功能概述

熟练掌握 MAT 是 Java 高手的必备能力,但实践时大家往往需面对众多功能,可以根据这个脑图去寻找一些使用MAT的切入点,分析出异常点。

img

菜单-各功能的使用入口

img

1 内存分布详解及实战

Overview

功能:展现堆内存大小、对象数量、class 数量、class loader 数量、GC Root 数量、环境变量、线程概况等全局统计信息。

使用入口:MAT 主界面 → Heap Dump Overview。

举例:下面是对象数量、class loader 数量、GC Root 数量,可以看出 class loader 存在异常。

img

使用入口:MAT 主界面 → Java Basics -> Thread Overview and stack。

下图是线程概况,可以查看每个线程名、线程的 Retained Heap、daemon 属性等。

image-20250116142503899

使用场景 全局概览呈现全局统计信息,重点查看整体是否有异常数据,所以有效信息有限,下面几种场景有一定帮助:

  • 方法区溢出时(Java 8后不使用方法区,对应堆溢出),查看 class 数量异常多,可以考虑是否为动态代理类异常载入过多或类被反复重复加载。
  • 方法区溢出时,查看 class loader 数量过多,可以考虑是否为自定义 class loader 被异常循环使用。
  • GC Root 过多,可以查看 GC Root 分布,理论上这种情况极少会遇到,笔者只在 JNI 使用一个存在 BUG 的库时遇到过。
  • 线程数过多,一般是频繁创建线程但无法执行结束,从概览可以了解异常表象,具体原因可以参考本文线程分析部分内容,此处不展开。

Dominator tree

使用频率top 1,是高效分析 Dump 必看的功能

功能

  • 展现对象的支配关系图,并给出对象支配内存的大小(支配内存等同于 Retained Heap,即其被 GC 回收可释放的内存大小)
  • 支持排序、支持按 package、class loader、super class、class 聚类统计

使用入口:全局支配树: MAT 主界面 → Dominator tree。

举例: 下图中查看 Dominator tree

image-20250116142731886

有些情况下可能看不太出来支配起点对象的 Reatained Heap 占用很大内存,这时可以按 class、package、class loader做聚合,进而定位目标。

image-20250116142918095

可以定位到String对象支配内存较多,然后结合代码进一步分析具体原因。

image-20250116143212144

在一些操作后定位到异常持有 Retained Heap 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下。

image-20250116143524870

Histogram 直方图

使用概率 Top 2

功能

  • 罗列每个类实例的数量、类实例累计内存占比,包括自身内存占用量(Shallow Heap)及支配对象的内存占用量(Retain Heap)。
  • 支持按对象数量、Retained Heap、Shallow Heap(默认排序)等指标排序;支持按正则过滤;支持按 package、class loader、super class、class 聚类统计,

使用入口:MAT 主界面 → Histogram;注意 Histogram 默认不展现 Retained Heap,可以使用计算器图标计算,如下图所示。

image-20250116144018648

使用技巧

  • Integer,String 和 Object[] 一般不直接导致内存问题。为更好的组织视图,可以通过 class loader 或 package 分组进一步聚焦,如下图。

    image-20250116144148218

  • 可以在 Histogram 的某个类继续使用 outgoing reference 查看对象分布,进而定位哪些对象是大头

    image-20250116144429449

List objects找到大对象后,可以点击Path To GC Roots或incoming reference,查看大数组的引用路径。

Leak Suspects

功能:具备自动检测内存泄漏功能,罗列可能存在内存泄漏的问题点。

使用入口:一般当存在明显的内存泄漏时,分析完Dump文件后就会展现,也可以如下图在 MAT 主页 → Leak Suspects。

使用场景:需要查看引用链条上占用内存较多的可疑对象。这个功能可解决一些基础问题,但复杂的问题往往帮助有限。

image-20250116144654696

点击Keywords中的“Details”,获取实例到GC Root的最短路径、dominator路径的详细信息

image-20250116144804972

image-20250116144819555

Top Consumers

功能:最大对象报告,可以展现哪些类、哪些 class loader、哪些 package 占用最高比例的内存,其功能 Histogram 及 Dominator tree 也都支持。

使用场景:应用程序发生内存泄漏时,查看哪些泄漏的对象通常在 Dump 快照中会占很大的比重。因此,对简单的问题具有较高的价值。

综合案例一

首先进入 Dominator tree,可以看出是 SameContentWrapperContainerProxy 对象与 main 线程两者持有99%内存不能释放导致 OOM。

img

img

先来看方向一,在 Heap Dump Overview 中可以快速定位到 Number of class loaders 数达50万以上,这种基本属于异常情况,如下图所示。

img

使用 Class Loader Explorer 分析工具,此时会展现类加载详情,可以看到有524061个 class loader。我们的案例中仅有ClassLoaderOOMOps 这样的自定义类加载器,所以很快可以定位到问题。

img

img

如果类加载器较多,不能确定是哪个引发问题,则可以将所有的 class loader对象按类做聚类,如下图所示。

img

Histogram 会根据 class 聚合,并展现对象数量级其 Shallow Heap 及 Retained Heap(如Retained Heap项目为空,可以点击下图中计算机的图标并计算 Retained Heap),可以看到 ClassLoaderOOMOps 有524044个对象,其 Retain Heap 占据了370M以上(上述代码是100M左右)。

img

使用 incoming references,可以找到创建的代码位置。

img

再来看方向二,同样在占据319M内存的 Obejct 数组采用 incoming references 查看引用路径,也很容易定位到具体代码位置。并且从下图中我们看出,Dominator tree 的起点并不一定是 GC根,且通过 Dominator tree 可能无法获取到最开始的创建路径,但 incoming references 是可以的。

img

2 对象间依赖详解及实战

References

注:笔者使用频率 Top2

功能:在对象引用图中查看某个特定对象的所有引用关系(提供对象对其他对象或基本类型的引用关系,以及被外部其他对象的引用关系)。通过任一对象的直接引用及间接引用详情(主要是属性值及内存占用),提供完善的依赖链路详情。

使用入口:目标域右键 → List objects → with outgoing references/with incoming references.

使用场景

  • outgoing reference:查看对象所引用的对象,并支持链式传递操作。如查看一个大对象持有哪些内容,当一个复杂对象的 Retained Heap 较大时,通过 outgoing reference 可以查看由哪个属性引发。下图中 A 支配 F,且 F 占据大量内存,但优化时 F 的直接支配对象 A 无法修改。可通过 outgoing reference 看关系链上 D、B、E、C,并结合业务逻辑优化中间环节,这依托 dominator tree 是做不到的。
  • incoming reference:查看对象被哪些对象引用,并支持链式传递操作。如查看一个大对象都被哪些对象引用,下图中 K 占内存大,所以 J 的 Retained Heap 较大,目标是从 GC Roots 摘除 J 引用,但在 Dominator tree 上 J 是树根,无法获取其被引用路径,可通过 incoming reference 查看关系链上的 H、X、Y ,并结合业务逻辑将 J 从 GC Root 链摘除。 img

Thread overview

功能:展现转储 dump 文件时线程执行栈、线程栈引用的对象等详细状态,也提供各线程的 Retained Heap 等关联内存信息。

使用入口:MAT 主页 → Thread overview

使用场景

  • 查看不同线程持有的内存占比,定位高内存消耗线程(开发技巧:不要直接使用 Thread 或 Executor 默认线程名避免全部混合在一起,使用线程尽量自命名方便识别,如下图中 ThreadAndListHolder-thread 是自定义线程名,可以很容易定位到具体代码)
  • 查看线程的执行栈及变量,结合业务代码了解线程阻塞在什么地方,以及无法继续运行释放内存,如下图中 ThreadAndListHolder-thread 阻塞在 sleep 方法。 img

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 引用链即可。

img

class loader 分析

功能

  • 查看堆中所有 class loader 的使用情况(入口:MAT 主页菜单蓝色桶图标 → Java Basics → Class Loader Explorer)。
  • 查看堆中被不同class loader 重复加载的类(入口:MAT 主页菜单蓝色桶图标 → Java Basics → Duplicated Classes)。

使用场景

  • 当从 Heap dump overview 了解到系统中 class loader 过多,导致占用内存异常时进入更细致的分析定位根因时使用。
  • 解决 NoClassDefFoundError 问题或检测 jar 包是否被重复加载

具体使用方法在 2.6 及 3.5 两节的案例中有介绍。

综合案例二

使用工具项:class loader(重复类检测)、inspector、正则检索。

异常现象 :运行时报 NoClassDefFoundError,在 classpath 中有两个不同版本的同名类。

分析过程

  1. 进入 MAT 已加载的重复类检测功能,方式如下图。 img

  2. 可以看到所有重复的类,以及相关的类加载器,如下图。 img

  3. 根据类名,在<Regex>框中输入类名可以过滤无效信息。

  4. 选中目标类,通过Inspector视图,可以看到被加载的类具体是在哪个jar包里。(本例中重复的类是被 URLClassloader 加载的,右键点击 “_context” 属性,最后点击 “Go Into”,在弹出的窗口中的属性 “_war” 值是被加载类的具体包位置)

    img img

3 对象状态详解及实战

inspector

功能:MAT 通过 inspector 面板展现对象的详情信息,如静态属性值及实例属性值、内存地址、类继承关系、package、class loader、GC Roots 等详情数据。

使用场景

  • 当内存使用量与业务逻辑有较强关联的场景,通过 inspector 可以通过查看对象具体属性值。比如:社交场景中某个用户对象的好友列表异常,其 List 长度达到几亿,通过 inspector 面板获取到异常用户 ID,进而从业务视角继续排查属于哪个用户,本例可能有系统账号,与所有用户是好友。
  • 集合等类型的使用会较多,如查看 ArrayList 的 size 属性也就了解其大小。

举例:下图中左边的 Inspector 窗口展现了地址 0x125754cf8 的 ArrayList 实例详情,包括 modCount 等并不会在 outgoing references 展现的基本属性。 img

集合状态

功能:帮助更直观的了解系统的内存使用情况,查找浪费的内存空间。

使用入口:MAT 主页 → Java Collections → 填充率/Hash冲突等功能。

img

使用场景

  • 通过对 ArrayList 或数组等集合类对象按填充率聚类,定位稀疏或空集合类对象造成的内存浪费。
  • 通过 HashMap 冲突率判定 hash 策略是否合理。

具体使用方法在 4.3 节案例详细介绍。

综合案例三

使用工具项:Dominator tree、Histogram、集合 ratio。

异常现象 :程序 OOM,且 Dominator tree 无大对象,通过 Histogram 了解到多个 ArrayList 占据大量内存,期望通过减少 ArrayList 优化程序。

分析过程

  1. 使用 Dominator tree 查看并无高占比起点。 img
  2. 使用 Histogram 定位到 ListHolder 及 ArrayList 占比过高,经过业务分析很多 List 填充率很低浪费内存。 img
  3. 查看 ArrayList 的填充率,MAT 首页 → Java Collections → Collection Fill Ratio。 img
  4. 查看类型填写 java.util.ArrayList。 img
  5. 从结果可以看出绝大部分 ArrayList 初始申请长度过大。 img