V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
nkduqi
V2EX  ›  Java

利用 MAT 玩转 JVM 内存分析(一)

  •  
  •   nkduqi · 2019-06-08 14:13:47 +08:00 · 3134 次点击
    这是一个创建于 1999 天前的主题,其中的信息可能已经有所发展或是发生改变。

    尽管 JVM 提供了自动内存管理的机制,试图降低程序员的开发门槛,确实也实现了这一目标,在日常开发中,我们一般都不需要关心对象的内存释放。JVM 大部分都是使用 trace 算法来判断一个对象是否该被回收,那么 JVM 只能回收那些从 gc roots 不可达的对象。

    如果我们在使用某些大的对象、集合对象或者一些三方包里的资源,忘记及时释放资源的话,还是会造成 JVM 的内存泄漏或内存浪费的问题。因此,如果想成为更高阶的 Java 开发工程师,我们需要了解常见的问题排查的办法和工具,这个系列的文章,准备介绍一个用来做 JVM 堆内存分析的工具—— MAT ( Memory Aanlysis Tool )。

    MAT 的官网在: https://www.eclipse.org/mat/,可以看下它的介绍—— MAT 是一款高性能、具备丰富功能的 Java 堆内存分析工具,可以用来排查内存泄漏和内存浪费的问题。

    一、安装和装设置

    1.1 mac 安装

    MAT 支持两种安装方式,一种是"单机版“的,也就是说用户不必安装 Eclipse IDE 环境,MAT 作为一个独立的 Eclipse RCP 应用运行;另一种是”集成版“的,也就是说 MAT 也可以作为 Eclipse IDE 的一部分,和现有的开发平台集成。

    这里我们考虑独立安装,在观望的下载页面,选择 mac os 版本的安装文件下载即可。

    MAT 的独立下载地址

    安装遇到的坑

    1. 启动直接报错,针对这个问题,我找到了这个答案:https://stackoverflow.com/questions/47909239/how-to-run-eclipse-memory-analyzer-on-mac-os,这个帖子里给出了两个方案:
      • 系统默认的 workspace 是只读的,更换掉即可。怎么更换呢,在文件/Applications/mat.app/Contents/Eclipse/MemoryAnalyzer.ini中进行修改。image.png
    • 在 mat 的安装目录下,我的机器是/Applications/mat.app/Contents/MacOS,执行./MemoryAnalyzer命令,这种只能通过命令启动,不能通过图表启动。

    关于方案 1,这篇文章讲得更细致: https://www.jianshu.com/p/9bbbe3c4cc8b

    1. 启动后,UI 界面没反应,参考: https://www.eclipse.org/forums/index.php/t/1090889/,换个包即可。这个问题我遇到过很多次。image.png

    1.2 mat 的设置

    配置 mat 的堆内存大小

    我的电脑是 8C16G 的,那理论上分析 10G 的堆文件没问题,但是 MAT 默认的配置没有这么大,需要在/Applications/mat.app/Contents/Eclipse/MemoryAnalyzer.ini文件中进行修改。如下图所示,我将我的 MAT 自己的运行时堆内存配置成了 6G。image.png

    配置 MAT 的使用

    MAT 的配置页面可以从 Window ——>Preferences 找到,如下图所示。 image.png

    MAT 的一般配置有几个选项

    1. Keep unreachable objects:如果勾选这个,则在分析的时候会包含 dump 文件中的不可达对象;

    2. Hide the getting started wizard:隐藏分析完成后的首页,控制是否要展示一个对话框,用来展示内存泄漏分析、消耗最多内存的对象排序。

    3. Hide popup query help:隐藏弹出查询帮助,除非用户通过 F1 或 Help 按钮查询帮助。

    4. Hide Welcome screen on launch:隐藏启动时候的欢迎界面

    5. Bytes Display:设置分析结果中内存大小的展示单位

    可以看出,MAT 不仅支持 HPROF 文件的分析,还支持 DTFJ 文件的分析。一般 sun 公司系列的 JVM 生成的 dump 文件都是 HPROF 格式的,IBM 的 JVM 生成的 dump 文件时 DTFJ 格式的。

    二、基本概念

    Heap Dump

    Heap Dump 是 Java 进程在某个时刻的内存快照,不同 JVM 的实现的 Heap Dump 的文件格式可能不同,进而存储的数据也可能不同,但是一般来说。

    Heap Dump 中主要包含当生成快照时堆中的 java 对象和类的信息,主要分为如下几类:

    • 对象信息:类名、属性、基础类型和引用类型

    • 类信息:类加载器、类名称、超类、静态属性

    • gc roots:JVM 中的一个定义,进行垃圾收集时,要遍历可达对象的起点节点的集合

    • 线程栈和局部变量:快照生成时候的线程调用栈,和每个栈上的局部变量

    Heap Dump 中没有包含对象的分配信息,因此它不能用来分析这种问题:一个对象什么时候被创建、一个对象时被谁创建的。

    Shallow vs. Retained Heap

    Shallow heap是一个对象本身占用的堆内存大小。一个对象中,每个引用占用 8 或 64 位,Integer 占用 4 字节,Long 占用 8 字节等等。

    Retained set,对于某个对象 X 来说,它的 Retained set 指的是——如果 X 被垃圾收集器回收了,那么这个集合中的对象都会被回收,同理,如果 X 没有被垃圾收集器回收,那么这个集合中的对象都不会被回收。

    Retained heap,对象 X 的 Retained heap 指的时候它的 Retained set 中的所有对象的 Shallow si 的和,换句话说,Retained heap 指的是对象 X 的保留内存大小,即由于它的存活导致多大的内存也没有被回收。

    leading set,对象 X 可能不止有一个,这些对象统一构成了 leading set。如果 leading set 中的对象都不可达,那么这个 leading set 对应的 retained set 中的对象就会被回收。一般有以下几种情况:

    1. 某个类的所有实例对象,这个类对象就是 leading object
    2. 某个类记载器加载的所有类,以及这些类的实例对象,这个类加载器对象就是 leading object
    3. 一组对象,要达到其他对象的必经路径上的对象,就是 leading object

    在下面这张图中,A 和 B 是 gc roots 中的节点(方法参数、局部变量,或者调用了 wait()、notify()或 synchronized()的对象)等等。可以看出,E 的存在,会导致 G 无法被回收,因此 E 的 Retained set 是 E 和 G ; C 的存在,会导致 E、D、F、G、H 都无法被回收,因此 C 的 Retined set 是 C、E、D、F、G、H ; A 和 B 的存在,会导致 C、E、D、F、G、H 都无法被回收,因此 A 和 B 的 Retained set 是 A、B、C、E、D、F、G、H。image.png

    Dominator Tree

    MAT 根据堆上的对象引用关系构建了支配树( Dominator Tree ),通过支配树可以很方便得识别出哪些对象占用了大量的内存,并可以看到它们之间的依赖关系。

    如果在对象图中,从 gc root 或者 x 上游的一个节点开始遍历,x 是 y 的必经节点,那么就可以说 x 支配了 y (dominate)。

    如果在对象图中,x 支配的所有对象中,y 的距离最近,那么就可以说 x 直接支配(immediate dominate) y。

    支配树是基于对象的引用关系图建立的,在支配树中每个节点都是它的子节点的直接支配节点。基于支配树可以很清楚得看到对象之间的依赖关系。

    现在看个例子,在下面这张图中

    1. x 节点的子树就是所有被 x 支配的节点集合,也正式 x 的 retained set ;
    2. 如果 x 是 y 的直接支配节点,那么 x 的支配节点也可以支配 y
    3. 支配树中的边跟对象引用图中的引用关系并不是一一对应的。

    image.png

    Garbage Collection Roots

    在 MAT 中,gc roots 的概念跟研究垃圾收集算法时候的概念稍微有点不同。gc roots 中的对象,是指那些可以从堆外访问到的对象的集合。如果一个对象符合下面这些场景中的一个,就可以被认为是 gc roots 中的节点:

    1. System Class:由 bootstrap classloader 加载的类,例如 rt.jar ,里面的类的包名都是java.util.*开头的。
    2. JNI Local:native 代码中的局部变量,例如用户编写的 JNI 代码或 JVM 内部代码。
    3. JNI Global:native 代码中的全局变量,例如用户编写的 JNI 代码或 JVM 内部代码。
    4. Thread Block:被当前活跃的线程锁引用的对象。
    5. Thread:正在存活的线程
    6. Busy Monitor:调用了 wait()、notify()或 synchronized 关键字修饰的代码——例如synchronized(object)synchronized方法。
    7. Java Local:局部变量。例如函数的输入参数、正在运行的线程栈里创建的对象。
    8. Native Stack:native 代码的输入或输出参数,例如用户定义的 JNI 代码或 JVM 的内部代码。在文件 /网络 IO 方法或反射方法的参数。
    9. Finalizable:在 finalize 队列中等待它的 finalizer 对象运行的对象。
    10. Unfinalized:重载了 finalize 方法,但是还没有进入 finalize 队列中的对象。
    11. Unreachable:从任何 gc roots 节点都不可达的对象,在 MAT 中将这些对象视为 root 节点,如果不这么做,就不能对这些对象进行分析。
    12. Java Stack Frame:Java 栈帧,用于存放局部变量。只在 dump 文件被解析的时候会将 java stack frame 视为对象。
    13. Unknown:没有 root 类型的对象。有些 dump 文件(例如 IBM 的 Portable Heap Dump )没有 root 信息。

    三、获取 Dump 文件

    1. 通过 MAT 生成 dump 文件 通过这个路径找到生成 dump 文件的对话框 image.png 选择一个进程,点击 finish 即可 image.png

    2. 通过 jmap 命令生成 dump 文件

      • 命令格式:jmap -dump:live,format=b,file=heap.bin <pid>
      • 注意:如果要保留 heapdump 中的不可达对象,则需要把”:live “去掉,即使用命令” jmap -dump,format=b,file=heap.bin <pid>“</pid>
    3. 通过设置 JVM 参数自动生成 使用-XX:+HeapDumpOnOutOfMemoryError这个 JVM 参数,在 Java 进程运行过程中发生 OOM 的时候就会生成一个 heapdump 文件,并写入到指定目录,一般用-XX:HeapDumpPath=${HOME}/logs/test来设置。


    本号专注于后端技术、JVM 问题排查和优化、Java 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。

    javaadu

    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3126 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 14:13 · PVG 22:13 · LAX 06:13 · JFK 09:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.