jvm(三)MAT基本使用
文章目录
内存泄露
- 内存泄露的原因
- 全局的容器类(如HashMap,或者自定义的容器类等),在对象不再需要时,忘记从容器中remove,这样这个对象就会仍然被HashMap等引用到,造成这个对象不满足垃圾回收的条件,从而造成内存泄漏。特别地,在抛出异常的时候,一定要确保remove被执行到。对集合对象(系统提供的或者自己实现的)只添加而不删除元素,在其它地方并保持了对集合对象的引用,是一种最常见的内存泄漏。
- 像Runnable对象等被JVM自身管理的对象,没有正确的释放渠道。Runnable对象必须交给一个Thread去run,否则该对象就永远不会消亡。因为像这种对象,尽管不被应用程序中的其它用户对象访问,但是这种对象会被JVM内部所引用
- 原子数据类型没有对象引用,因为所有的原子数据类型是放在一些对象里的,或者都是暂态临时对象。原子对象的复制,执行的是拷贝操作,而不是指向操作
- Java内存泄露的症状
- 系统越来越慢,并伴随CPU使用率过高。这主要是因为随着内存的泄漏,可用的内存越来越小,垃圾回收器频频进行垃圾回收(完全垃圾回收(FULL GC)一次接一次,每次耗时几秒,甚至几十秒),而垃圾回收一个CPU密集型操作,频繁的GC会导致CPU持续居高不下,在有内存泄漏的场合,到了最后必然是伴随着CPU使用率几乎为100
- 系统运行一段时间,系统抛OutOfMemory异常,至此整个系统完全不工作
- 虚拟机core dump
- 内存泄露的定位与分析:内存泄漏的分析过程并不复杂。但往往需要很大的耐心,因为内存泄漏的分析只能是事后分析,问题重现后才可以进行分析,常用的工具就是MAT
配置
-
配置页面
-
Keep unreachable objects
:分析的时候会包含dump文件中的不可达对象; -
Hide the getting started wizard
:隐藏分析完成后的首页,控制是否要展示一个对话框,用来展示内存泄漏分析、消耗最多内存的对象排序。 -
Hide popup query help
:隐藏弹出查询帮助,除非用户通过F1或Help按钮查询帮助。 -
Hide Welcome screen on launch
:隐藏启动时候的欢迎界面 -
Bytes Display
:设置分析结果中内存大小的展示单位
-
-
Memory Analyzer
目前支持三种转储文件类型-
IBM Portable Heap Dump (PHD)
:这个专有的 IBM 格式只包含进程中每个 Java 对象的类型和大小,以及这些对象之间的关系。这个转储文件格式远远小于其他格式,并且只包含最少的信息。 -
HPROF二进制转储文件
: HPROF 二进制转储文件在 IBM PHD 格式中包含了所有数据表现方式,以及 Java 对象和线程内部的基本数据类型,您可以查看对象中域的值,查看在转储文件产生时有哪些方法在被执行。这些数据使 HPROF 转储文件明显比 PHD 格式的转储文件要大;它们大约与所使用的 Java 堆一样大。一般sun公司系列的JVM生成的dump文件都是HPROF格式的 -
DTFJ二进制转储文件
:IBM的JVM生成的dump文件时DTFJ格式的
-
基础概念入门
-
Shallow Heap(浅堆)
:-
Shallow Size
就是对象本身占用内存的大小,不包含其引用的对象(在32位系统中,一个对象引用会占据4个字节,一个int占4个字节,long占用8个字节,每个对象头占用8个字节)。常规对象(非数组)的Shallow Size
由其成员变量的数量和类型决定。数组的Shallow Size
由数组元素的类型(对象类型、基本类型)和数组长度决定 - java的对象成员都是些引用,真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],所以我们如果只看对象本身的内存,那么数量都很小。所以我们看到Histogram图是以Shallow size进行排序的,排在第一位第二位的是byte,char 。
-
-
Retained Set(保留集)
:对于某个对象X来说,它的Retained Set
指的是——如果X被垃圾收集器回收了,那么这个集合中的对象都会被回收,同理,如果X没有被垃圾收集器回收,那么这个集合中的对象都不会被回收。 -
Leading Set
:对象X可能不止有一个,这些对象统一构成了Leading Set
。如果Leading Set
中的对象都不可达,那么这个Leading Set
对应的Retained Set
中的对象就会被回收 -
Retained Heap(保留堆)
:表示如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小。于是,如果一个对象的某个成员new了一大块int数组,那这个int数组也可以计算到这个对象中。相对于Shallow Heap
,Retained Heap
可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,Retained Heap
都可以被释放)
对于以上的图,看似可以得出结论每个对象的Retained Size
=Shallow Size
+直接子对象的 Retained Size
。对于以下的图,Obj2的Retained Size
不再符合刚刚总结出来的公式,这是因为Obj2的直接子对象Obj5还被Obj6所引用,造成的结果就是,如果Obj2被回收,Obj5并不会被回收,所以 Obj2 的Retained Size
就不应该包括 Obj5 的Retained Size
. 虽然 Obj2 的 Retained Size 发生了变化,但是 Obj1 的Retained Size
并没有发生变化。
针对不可达对象(下一次GC会被清理的对象),也就是可以完全被清除的对象,Retained Size 都是0
实际案例分析
-
模拟产生内存泄露的代码
/** * 运行时增加jvm参数 * -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Volumes/P/temp/OomHeapForMat.hprof.dump -Xms20M -Xmx20M * **/ public class OomHeapForMat { public static void main(String[] args) { ArrayList<Person> personList = new ArrayList<>(); for (int i = 0; i < 1000000; i++) { Person person = new Person("jannal" + i, i); personList.add(person); } } } class Person { private String username; private int age; public Person(String username, int age) { this.username = username; this.age = age; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
-
控制台输出
java.lang.OutOfMemoryError: GC overhead limit exceeded Dumping heap to /Volumes/P/temp/OomHeapForMat.hprof.dump ... Heap dump file created [27082418 bytes in 0.227 secs] Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at cn.jannal.jvm.mat.OomHeapForMat.main(OomHeapForMat.java:14)
-
也可以通过jmap生成堆快照
jps查找pid jmap -dump:live,format=b,file=/Volumes/P/temp/OomHeapForMat.hprof.dump 10862
-
MAT文件打开输出
或者通过
Acquire Heap Dump
打开本地正在运行进程的dump输出目录会生成很多文件
- *.zip是打包的报告
概览页面
-
概览页面
-
Histogram可以列出内存中的对象,对象的个数以及大小。
-
Dominator Tree可以列出哪个线程,以及线程下面的那些对象占用的空间。
-
Top consumers通过图形列出最大的object。
-
Leak Suspects通过MA自动分析泄漏的原因。
-
Histogram
-
Histogram 可列出每一个类的实例数,支持正则表达式
-
Class Name
:类的全限定名 -
Objects
:类的对象的数量,这个对象被创建了多少个 -
Shallow Heap
:一个对象内存的消耗大小,不包含对其他对象的引用。针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包括一些java语言特性的数据存储单元。针对数组类型的对象,它的大小是数组元素对象的大小总和。 -
Retained Heap
: Retained Heap就是当前对象被GC后,从Heap上总共能释放掉的内存。不过,释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被被当做Garbage。可以通过正则表达式过滤自定义包中的类
-
-
对于给定的一个对象,MAT可以找到引用当前对象的对象以及当前对象引用的对象。在某一项上右键打开菜单选择 list objects :
-
with incoming references 将列出哪些类引入该类;
-
with outgoing references 列出该类引用了哪些类
从下图可以看出ArrayList类持有了Person对象的引用,并且该List大小为208889
-
-
按照不同的组显示视图,如按照package分组,这样可以快速查找自己定义包中的类,便于快速定位
Dominator Tree
-
Dominator Tree(支配树)是一个对象图, 它将对象的引用关系转换成一种树形的对象图结构. 通过它可以很轻松地看出对象的引用关系以及查看heap dump中占用内存最大的对象。在支配树中,对于某一层的节点来说,如果它们的parent节点没有被其他对象引用了,那么这些节点都会被垃圾收集器回收。支配树可以用来排查是哪些对象导致了其他对象无法被垃圾收集器回收,跟类直方图类似,支配树也从类加载器、package等角度来看
-
GC ROOT:通常GC Roots是一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。所以GC Roots是分析对象为何还存活于内存中的利器。
-
查看一个对象到RC Roots的引用链。通常在排查内存泄漏的时候,我们会选择exclude all phantom/weak/soft etc.references,意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump之前要手动出发GC来保证),如果有,则说明存在内存泄漏,然后再去排查具体引用。
-
在MAT中,gc roots的概念跟研究垃圾收集算法时候的概念稍微有点不同。gc roots中的对象,是指那些可以从堆外访问到的对象的集合。如果一个对象符合下面这些场景中的一个,就可以被认为是gc roots中的节点:
- System Class:由bootstrap classloader加载的类,例如rt.jar,里面的类的包名都是
java.util.*
开头的。 - JNI Local:native代码中的局部变量,例如用户编写的JNI代码或JVM内部代码。
- JNI Global:native代码中的全局变量,例如用户编写的JNI代码或JVM内部代码。
- Thread Block:被当前活跃的线程锁引用的对象。
- Thread:正在存活的线程
- Busy Monitor:调用了wait()、notify()或synchronized关键字修饰的代码——例如
synchronized(object)
或synchronized
方法。 - Java Local:局部变量。例如函数的输入参数、正在运行的线程栈里创建的对象。
- Native Stack:native代码的输入或输出参数,例如用户定义的JNI代码或JVM的内部代码。在文件/网络IO方法或反射方法的参数。
- Finalizable:在finalize队列中等待它的finalizer对象运行的对象。
- Unfinalized:重载了finalize方法,但是还没有进入finalize队列中的对象。
- Unreachable:从任何gc roots节点都不可达的对象,在MAT中将这些对象视为root节点,如果不这么做,就不能对这些对象进行分析。
- Java Stack Frame:Java栈帧,用于存放局部变量。只在dump文件被解析的时候会将java stack frame视为对象。
- Unknown:没有root类型的对象。有些dump文件(例如IBM的Portable Heap Dump)没有root信息。
- System Class:由bootstrap classloader加载的类,例如rt.jar,里面的类的包名都是
-
一个对象Y从其开始(可能是GC Roots)到其节点的每条路径都必须经过X对象的话, 那么就是X支配(dominates)Y。离Y最近的主宰对象, 称之为Y的直接支配(immediate dominator)。在Dominator Tree(支配树)中, 每个对象都是其子对象的直接支配, 故而对象的引用关系也很好定位
-
Dominator Tree(支配树)包含如下几点特征
- X的子树的所有对象, 即是X的Retained Set。
- 如果X是Y的直接支配, 那么X的直接支配肯定也支配Y, 以此类推
- Dominator Tree(支配树)中的线并不直接对应对象图中对象的引用关系, 例如下图中的D和F的关系
Leak Suspects
-
Leak Suspects
:用于排查潜在的内存泄露问题从这份报告,看到该图深色区域(Problem Suspect 1)被怀疑有内存泄漏。整个堆20M,这块占用了97.61%.被怀疑是内存泄露,查看Details(如下图),
Accumulated Objects in Dominator Tree
可以看出ArrayList占用内存比例最大,并且ArrayList本身以及内部数组的占用的内存较小(从shallow Heap大小可以看出),而引用的对象的内存(Retained Heap)占了18.45MB。数组中的cn.jannal.jvm.mat.Person
对象本身占用内存也比较小。继续向下查看Accumulated Objects by Class in Dominator Tree
显示Person对象被208,889次,累加使用的堆大小是18,381,432,所以可以得出结论,内存溢出的原因是Person被创建的太多,但是一直没有释放,最终导致堆内存(20MB)不够。
Top Components
-
针对那些占用堆内存超过整个堆内存1%大小的组件做一系列的分析
Thread视图
-
线程视图:在生成快照的时刻,JVM中java线程对象列表。这里显示的是java层面的应用线程。无法显示JVM的线程。通过线程的堆栈可以查看局部变量的信息,带有
<local>
标记的,就为当前栈帧的局部变量第二种访问线程视图的方式
HeapDump视图
-
Headp Dump Overview视图是一个组合视图,有常见的一些分析功能
比较多个HeapDump文件
-
将上面的程序修改一下,生成一个新的文件
/** * -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Volumes/P/temp/OomHeapForMat2.hprof.dump -Xms20M -Xmx20M **/ public static void main(String[] args) throws Exception { //为了争取dump堆的时间,所以休眠10s, Thread.sleep(10000); ArrayList<Person> personList = new ArrayList<>(); for (int i = 0; i < 1000000; i++) { Person person = new Person("jannal" + i, i); personList.add(person); if (i == 1000) { personList.clear(); } System.gc(); } } 10s后使用 jmap -dump:live,format=b,file=/Volumes/P/temp/OomHeapForMat2.hprof.dump 86408 生成堆文件
-
调试内存泄露时,有时候适时比较2个或多个heap dump文件是很有用的。这时需要生成多个单独的HPROF文件。
-
步骤
-
打开第一个
OomHeapForMat.hprof.dump
文件,并打开Histogram view
-
在Navigation History view选择Add to Compare Basket
-
重复以上步骤打开第二个
OomHeapForMat2.hprof.dump
文件 -
点击比较结果,
OomHeapForMat.hprof.dump
与OomHeapForMat2.hprof.dump
相比,内存中Person对象个数明显多了很多,内存占用也多了很多
-