Shallow Size和Retained Size
Shallow Size
Shallow Size是指对象自身占用的内存大小,不包括它引用的对象。
针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包含一些java语言特性的数据存储单元。
针对数组类型对象,它的大小是数组元素对象的大小总和(由对象类型和数组长度决定)。
Retained Size
Retained Size = 当前对象大小 + 当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C,C是间接引用)。
换句话说,Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。
不过,释放的时候,还要排查被GC Roots直接或间接引用的对象,他们暂时不会被当前为Garbage。
看图理解Retained Size
上图中,GC Roots直接引用了A和B两个对象。
A对象的Retained Size=A对象的Shallow Size。
B对象的Retained Size=B对象的Shallow Size + C对象的Shallow Size
这里不包括D对象,因为D对象被GC Root直接引用。
如果GC Roots不引用D对象呢?
此时,B对象的Retained Size=B对象的Shallow Size + C对象的Shallow Size + D对象的Shallow Size。
GC Roots
当GC发现通过任何引用链都无法访问某个对象的时候,该对象将被回收,GC Roots是分析这一过程的起点。通常GC Roots是一个当前线程调用栈上的对象(例如方法参数和局部变量),或者是线程自身,或者是系统类加载器加载的类以及Native code(本地代码)保留的活动对象。
内存分析
1. 观察Heap(动态)
在DDMS(Dalvik Debug Monitor Server)内,点击heap按钮,更新统计信息。
我们主要关注两项数据:
Heap Size 堆的大小,当资源增加,当前堆的空余空间不够时,系统会增加堆的大小,若超过上限 (例如 64M,视平台和具体机型而定)则会被杀掉。
Allocated 堆中已分配的大小,这是应用程序实际占用的内存大小,资源回收后,此项数据会变小
查看操作前后的堆数据,看是否有内存泄漏
2. 利用MAT(Memory Analyzer Tool)分析内存堆(静态)
JVM能够记录下问题发生时系统的部分运行状态,并将其存储在堆转储(Heap Dump)文件中,从而为我们分析和诊断问题提供了重要的依据。
DDMS可以将当前的内存 Dump成一个 hprof格式的文件,MAT 读取这个文件后会给出方便阅读的信息。
可以点击工具栏上的Leak Suspects菜单项来生成内存泄露分析报告,也可以直接点击饼图下方的Reports->Leak Suspects链接来生成报告。
分析三步曲
通常我们会采用下面的“三步曲”来分析内存泄漏问题:
首先,对问题发生时刻的系统内存状态获取一个整体印象。
第二步,找到最有可能导致内存泄漏的元凶,通常也就是消耗内存最多的对象。
接下来,进一步去查看这个内存消耗大户的具体情况,看看是否有什么异常行为。
2.1 Leak Suspects
Leak Suspects中会给出了MAT认为可能出现内存泄漏问题的地方,生成一个内存泄漏的OverView界面,获取一个内存状态的整体印象。
分析内存泄漏的原因。
2.2 Histogram
Histogram: 列出内存中的对象,对象的个数及大小。
它按类名将所有的实例对象列出来,可以点击表头进行排序,在表的第一行可以输入正则表达式来匹配结果。
2.3 Dominator Tree
Dominator Tree (支配树):列出最大的对象以及其依赖存活的Object (大小是以Retained Heap为标准排序的)
Retained Set指的是这个对象本身和他持有引用的对象以及这些引用对象的Retained Set所占内存大小的总和,官方的图解如下所示。
从图中可以看出E的Retained Set为E和G。C的Retained Set为C、D、E、F、G、H。
MAT所定义的支配树就是从上图的引用树演化而来。在引用树当中,一条到Y的路径必然会经过X,这就是X支配Y。X直接支配Y则指的是在所有支配Y的对象中,X是Y最近的一个对象。支配树就是反映的这种直接支配关系,在支配树中,父节点直接支配子节点。下图就是官方提供的一个从引用树到支配树的转换示意图。
C直接支配D、E,因此C是D、E的父节点,这一点根据上面的阐述很容易得出结论。C直接支配H,这可能会有些疑问,能到达H的主要有两条路径,而这两条路径FD和GE都不是必须要经过的节点,只有C满足了这一点,因此C直接支配H,C就是H的父节点。通过支配树,我们就可以很容易的分析一个对象的Retained Set,比如E被回收,则会释放E、G的内存,而不会释放H的内存,因为F可能还引用着H,只有C被回收,H的内存才会被释放。
这里对支配树进行了讲解,我们可以得出一个结论:通过MAT提供的Dominator Tree,可以很清晰的得到一个对象的直接支配对象,如果直接支配对象中出现了不该有的对象,就说明发生了内存泄漏。
2.4 Top Consumers
Top Consumers: 通过图形列出最大的object。
通常,Histogram和 Dominator Tree是最常用的。
2.5 outgoing references和incoming References
outgoing references:表示该对象的出节点(被该对象引用的对象)。
incoming references:表示该对象的入节点(引用到该对象的对象)。
2.6 OQL语句查询
OQL全称为Object Query Language,类似于SQL语句的查询语言,能够用来查询当前内存中满足指定条件的所有的对象。它的查询语句的基本格式为:
SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression>]
在Eclipse Memory Tool 上,按OQL,输入select * from instanceof android.app.Activity, 这个指令可以找所有在系统上是android.app.Activity的instance。
关于OQL语句有很多用法,具体可以查看官方文档
【http://help.eclipse.org/luna/index.jsp?topic=/org.eclipse.mat.ui.help/reference/oqlsyntax.html】。
如果在object的旁带有“Unknown”的话,那object是可以被Gargage Collect的。要看其他的object是什么原因不可以被Gargage Collect 的话,可以在那个object上right-click,然后选Path to GC Roots, exclude all phantom/weak/soft etc. references。 (这个选项排除了虚引用、弱引用和软引用,这些引用都是可以被VM查到的,所以是可以Gargage Collect的。)
结果中this$0的含义就是内部类自动保留的一个指向所在外部类的引用。
3. Native内存定位方法
调试应用本地内存泄露跟java层内存泄露一样,无法准确判定是哪段代码出现内存泄露,只能定位出疑似内存泄露点,需要结合代码来确定。
像分析java层heap文件一样,先从内存分配较大的代码段入手,结合分配路径和实际代码,判断是否有内存泄露。
具体的方法有:
方法一: 调试log的level
#adb root
#adb shell setprop libc.debug.malloc 40 [Default is 0]
设置想要调试的进程
#adb shell setprop libc.debug.malloc.program mm-qcamera-daemon
可选的配置项
#adb shell setprop libc.debug.malloc.minalloclim 10240 [Default is 10KB; values are in Bytes]
#adb shell setprop libc.debug.malloc.maxprocsize 31457280 [Default is 30 MB; values are in Bytes]
重启framework层
#adb shell stop
#adb shell start
或者重启进程
#adb shell kill <pid_of_mm-qcamera-daemon>
发送debug信号开始抓取
#adb shell kill -28 <pid_of_mm-qcamera-daemon>
调试log的level
#adb root
#adb shell setprop libc.debug.malloc 40 [Default is 0]
设置想要调试的进程
#adb shell setprop libc.debug.malloc.program mm-qcamera-daemon
可选的配置项
#adb shell setprop libc.debug.malloc.minalloclim 10240 [Default is 10KB; values are in Bytes]
#adb shell setprop libc.debug.malloc.maxprocsize 31457280 [Default is 30 MB; values are in Bytes]
重启framework层
#adb shell stop
#adb shell start
或者重启进程
#adb shell kill <pid_of_mm-qcamera-daemon>
发送debug信号开始抓取
#adb shell kill -28 <pid_of_mm-qcamera-daemon>
方法二:
adb shell setprop libc.debug.malloc 1
adb shell stop
adb shell start
adb shell am dumpheap -n
会得到一个.hprof文件,需要手动分析。
该文件上半部分是内存分配的代码段,按内存分配大小排序,下半部分是该进程的内存分配映像。
方法三: 使用DDMS
adb shell setprop libc.debug.malloc 1
adb shell stop
adb shell start
然后在DDMS中有个Native Heap的菜单,可以拿到相应的调试数据
参考链接
使用 Eclipse Memory Analyzer 进行堆转储文件分析