简介
MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。
除了Eclipse插件版,MAT也有独立的不依赖Eclipse的版本,只不过这个版本在调试Android内存的时候,需要将DDMS生成的文件进行转换,才可以在独立版本的MAT上打开。因为DDMS生成的是Android格式的HPROF(堆转储)文件,而MAT只能识别JAVA格式的HPROF文件。不过Android SDK中已经提供了这个Tools,所以使用起来也是很方便的。
要调试内存,首先需要获取HPROF文件,HPROF文件存储的是特定时间点,java进程的内存快照。有不同的格式来存储这些数据,总的来说包含了快照被触发时java对象和类在heap中的情况。由于快照只是一瞬间的事情,所以heap dump中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。
MAT涉及的词汇介绍
要看懂MAT的列表信息,Shallow heap、Retained Heap、GC Root这几个概念一定要弄懂。
Shallow heap
Shallow size就是对象本身占用内存的大小,不包含其引用的对象。
- 常规对象(非数组)的Shallow size有其成员变量的数量和类型决定。
- 数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定。
因为不像c++的对象本身可以存放大量内存,java的对象成员都是些引用
。真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],所以我们如果只看对象本身的内存,那么数量都很小。所以我们看到柱状图是以Shallow size进行排序的,排在第一位第二位的一般是byte,char 。
Retained Set
持有对象集合:
Retained Set是指当一个对象被GC时,会因为对象的释放而同时被GC掉的所有对象的集合;或者说是对象本身和对象所引用对象的集合;
Retained Heap
Retained Heap则表示一个对象的Retained Set中所有对象的Shallow Size的总和。换句话说,Retained Heap就表示如果一个对象被释放掉,那会因此而被释放的总的heap大小。于是,如果一个对象的某个成员new了一大块int数组,那这个int数组也可以计算到这个对象中。相对于shallow heap,Retained heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)。
这里要说一下的是,Retained Heap并不总是那么有效。例如我在A里new了一块内存,赋值给A的一个成员变量。此时我让B也指向这块内存。此时,因为A和B都引用到这块内存,所以A释放时,该内存不会被释放。所以这块内存不会被计算到A或者B的Retained Heap中。为了纠正这点,MAT中的Leading Object(例如A或者B)不一定只是一个对象,也可以是多个对象(Leading Set)。此时,(A, B)这个组合的Retained Set就包含那块大内存了。
很显然,从上面的对象引用图计算Retained Memory并不那么直观高效。比如A和B的Retained Memory只有它们自身,而E的Retained Memory则是E和G,G的Retained Memory也只是它自身。为了更直观的计算Retained Memory,MAT引入了Dominator(统治者) Tree的概念。
Dominator(统治者) Tree
在Dominator Tree中,有下面一些非正式的定义:
- 在对象图中,若每一条从开始节点(或根节点)到节点y的路径都必须经过节点x,那么节点x就dominates节点y。
- 在Dominator Tree中,每一个节点都是其子节点的直接Dominator(统治者)。
Dominator Tree还有以下重要属性:
- x的sub-tree就代表了x的retained set。
- 如果x是y的直接dominator,那么x的直接dominator同样dominates y,以此类推。
- Dominator Tree的边缘并不直接对应于对象引用树中的引用关系。比如在引用图中,C是A和B的子节点,而在Dominator Tree中,三者却是平级的。
对应到MAT UI上,在dominator tree这个view中,显示了每个对象的shallow heap和retained heap。而点击右键,可以List objects中选择with outgoing references和with incoming references。这是个真正的引用图的概念,
- outgoing references :表示该对象的出节点(被该对象引用的对象)。
- incoming references :表示该对象的入节点(引用到该对象的对象)。
GC ROOTS
首先要说一下GC的原则:
垃圾回收器会尝试回收所有非GC roots的对象。所以如果你创建一个对象,并且移除了这个对象的所有指向,它就会被回收掉。但是如果你将一个对象设置成GC roots,那它就不会被回收掉。
那么GC又如何判断某个对象是否可以被回收呢?
安卓的垃圾回收是通过可达性分析来搞定的,也就是说,从GC Roots 到这个对象不可达时,垃圾回收器就会释放掉它。
一个GC root就是一个对象,这个对象从堆外可以访问读取。以下一些方法可以使一个对象成为GC root:
- System class:被Bootstrap或者system class loader加载的类,比如位于rt.jar里的所有类(如java.util.*);
- JNI local:native代码里的local变量,比如用户定义的JNI代码和JVM的内部代码;
- JNI global:native代码里的global变量,比如用户定义的JNI代码和JVM的内部代码;
- Thread block:当前活跃的线程block中引用的对象;
- Thread:已经启动并且没有stop的线程;
- busy monitor:调用了wait()或者notify()或者被同步的对象,比如调用了synchronized(Object) 或使用了synchronized方法。静态方法指的是类,非静态方法指的是对象;
- java local:local变量,比如仍然存在于线程栈中的方法的入参和方法内创建的对象;
- native stack:native代码里的出入参数,比如file/net/IO方法以及反射的参数;
- finalizable:在一个队列里等待它的finalizer 运行的对象;
- unfinalized:一个有finalize方法的对象,还没有被finalize,同时也没有进入finalizer队列等待finalize;
- unreachable:被MAT标记为root,并且无法通过任何其他root到达的对象,这个root的作用是retain那些不这么做就无法包含在分析中的objects;
- java stack frame:一个持有本地变量的java栈帧。只有在dump被解析且在preferences里设置把栈帧当做对象对待时才会产生;
- unknown:未知root类型的对象。
Java的引用级别
从最强到最弱,不同的引用(可到达性)级别反映了对象的生命周期。
强引用Strong Reference:通过 new 关键字创建出来的对象引用都是强引用,只有去掉强引用,对象才会被回收。请记住,JVM宁可抛出OOM也不会去回收一个有强引用的对象。
软引用Soft Reference:只要有足够的内存,就一直保持对象,直到发现一次GC后内存仍然不够,系统会在将要发生OOM之前针对此类对象进行二次回收。如果此次回收还没有足够的内存,才会抛出OOM。一般可用来实现缓存,通过java.lang.ref.SoftReference类实现。
弱引用Weak Reference :比Soft Ref更弱,被弱引用关联的对象只能生存到下一次GC发生之前。在GC执行时,无论当前内存是否足够,都会立刻回收只被弱引用关联的对象。通过java.lang.ref.WeakReference和java.util.WeakHashMap类实现。
虚引用Phantom Reference:也成为幽灵引用或幻影引用。虚引用完全不会影响对象的生存时间,你只能使用Phantom Ref本身,而无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被GC时收到一个系统通知。一般用于在进入finalize()方法后进行特殊的清理过程,通过 java.lang.ref.PhantomReference实现。