详解Android内存泄漏检测与MAT使用
内存泄漏基本概念
内存检测这部分,相关的知识有jvm虚拟机垃圾收集机制,类加载机制,内存模型等。编写没有内存泄漏的程序,对提高程序稳定性,提高用户体验具有重要的意义。因此,学习java利用java编写程序的时候,要特别注意内存泄漏相关的问题。虽然jvm提供了自动垃圾回收机制,但是还是有很多情况会导致内存泄漏。
内存泄漏主要原因就是一个生命周期长的对象,持有了一个生命周期短的对象的引用。这样,会导致短的对象在该回收时候无法被回收。android中比较典型的有:1、静态变量持有activity的context。2、或者handler持有某个组件的context,同时如果looper的消息队列中有针对该handler的消息没有被处理,那么会被作为target持有强引用,最终的导致context无法释放,导致相应组件在退出时无法被内存回收。3、非静态内部类默认持有外部类的引用,这样如果我们在activity中定义了一个thread内部类,同时直接通过new thread的方式去运行线程,那么在线程运行结束之前,线程都会持有activity的引用,从而导致activity无法被释放。
内存检测工具
leakcananry
leakcanary,主要监测的是使用过程中activity,fragment等组件是否没被内存回收。使用方法也十分简单,相当于装了一个监听器,然后通过正常 操作去寻找内存泄漏,发生内存泄漏的时候会有toast,同时可以在相应程序查看哪里发生内存泄漏。
方法比较简单,添加leakcanary依赖以后,新建一个application入口,在oncreate方法中安装leakcanary即可。
当发生内存泄漏时,屏幕会出现toast,同时打开桌面上的leaks程序,显示泄漏的内存,如下图:
leakcananry实现步骤大致是:
实现大致步骤是:
1、自动把activity加入到keyedweakreference
2、在background线程中,检查ondestroy后reference是否被清除,且没有触发gc
3、如果reference没有被清除,则dump heap到一个hprof文件并保存到app文件系统中
4、在一个单独进程中启动heapanalyzerservice,heapanalyzer使用haha来分析heap dump。
5、heapanalyzer在heap dump中根据reference key找到keyedweakreference。
6、heapanalyzer计算出到gc roots的最短强引用路径来判断是否存在泄露,然后build出造成这个泄露的引用链。
7、结果被传回来app进程的displayleakservice,并展示一个泄露的notification。
方法的有点是简单易行,但是只能检测activity、fragment是否发生内存泄漏。
观看整体内存使用情况
详情参见官方文档: https://developer.android.com/studio/profile/investigate-ram.html#viewingallocations
使用adb shell,进入手机adb,执行命令:
dumpsys meminfo <包名> [-参数]
可以查看应用不同部分内存分配情况。比如java heap,native heap等
输出是目前具体应用的内存分配,单位是kilobytes
因为程序涉及jni,经常会分配本地内存,所以会使用adb shell 的方式去查看native heap的分配情况。
结果如下:
分析各个参数:
private clean/dirty ram:
这部分内存是app的私有内存,当app销毁是操作系统可以回收到的内存。其中private dirty只能被你的进程使用,同时只能存在在内存当中,当内存不够,也不能通过分页技术存储到硬盘(操作系统相关知识),dalvik和native heap上的分配都是private dirty ram。因为是dalvik heap和native heap共享的内存,所以命名dirty?
ddms
使用流程
- 启动eclipse后,切换到ddms透视图,并确认devices视图、heap视图都是打开的;
- 将手机通过usb链接至电脑,链接时需要确认手机是处于“usb调试”模式,而不是作为“massstorage”;
- 链接成功后,在ddms的devices视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息;
- 点击选中想要监测的进程,比如system_process进程;
- 点击选中devices视图界面中最上方一排图标中的“update heap”图标;
- 点击heap视图中的“cause gc”按钮;
- 此时在heap视图中就会看到当前选中的进程的内存使用量的详细情况。
如何检测内存泄漏?
heap视图中部有一个type叫做dataobject,即数据对象,也就是我们的程序中实例化的对象。在data object一行中有一列是“total size”,其值就是当前进程中所有java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。
正常情况下total size值都会稳定在一个有限的范围内,也就是说没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行gc的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平。如果代码中存在没有释放对象引用的情况,则dataobject的total size值在每次gc后不会有明显的回落,随着操作次数的增多total size的值会越来越大
通过ddms方式,dataobject 的totalsize如果稳定在一个大概范围内,则可以确定没有发生内存泄漏。
mat
然而,并不是所有的内存泄漏都十分明显,并且会最终导致oom。有时候只有几个对象被泄漏,虽然影响不大,但是无疑浪费了内存。
要发现这种比较隐蔽的内存泄漏,我们需要使用mat工具。
在了解支配树之前,要先了解一些相关概念。
支配树
支配树体现了对象实例间的支配关系,在对象引用图中,所有指向对象b的路径都经过对象a,则认为对象a支配对象b。
在这张图里,左边是对象引用关系,对于a和b,要抵达这两个点必须经过gc root。而对于c可以从a也可以从b抵达,但都必须经过gc root,所以最近的支配点同样也是gc root。
对于点d,不管是从c->d还是c->d->f->d,都必须经过的最近的点是c,所以c是d的支配点。同理可得efhg在支配树中的位置。
shallowheap和retained heap
shallow heap表示对象本身所占内存大小,一个内存大小100bytes的对象shallow heap就是100bytes。
retained heap表示通过回收这一个对象总共能回收的内存,比方说一个100bytes的对象还直接或者间接地持有了另外3个100bytes的对象引用,回收这个对象的时候如果另外3个对象没有其他引用也能被回收掉的时候,retained heap就是400bytes。
在使用mat进行分析时,我们常常接触到的数据就是shallow size和retained size: shallow size
对象自身占用的内存大小,不包括它引用的对象。
针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包括一些java语言特性的数据存储单元。
针对数组类型的对象,它的大小是数组元素对象的大小总和。
retained size
retained size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:a->b->c, c就是间接引用)
换句话说,retained size就是当前对象被gc后,从heap上总共能释放掉的内存。
不过,释放的时候还要排除被gc roots直接或间接引用的对象。他们暂时不会被回收。如下图:
a对象的retained size=a对象的shallow size
b对象的retained size=b对象的shallow size + c对象的shallow size
因为b对象被释放时,c同时被释放,而d由于被gc roots直接引用所以不会被释放。而retained size就是当前对象被gc后,从heap上总共能释放掉的内存。
以上概念,都是在使用mat进行内存分析经常使用的,所以要记住。
mat的下载与使用
下载地址:
这里没有作为eclipse插件的方式下载mat,而是通过下载单独的软件客户端。
首先,在ddms中选择要检测的进程并dump hprof file,如下图:
hprof中存储的是当前内存的快照,因此,在dump快照之前先点击cause gc手动触发一次垃圾回收,这样可以避免软引用、弱引用等不必要的对象保留在内存中影响我们的分析。
转储出来的hprof文件,还有使用sdk自带工具进行一下格式转化,工具在sdk路径下的platform-tools下,名称为hprof-conv。
使用方法:
/.hprof-conv.exe a.hprof b.hprof
a 是输入hprof文件名,b是输出文件名。
然后将b.hprof在eclipse memory analyzer中打开,注意要转换格式,不然无法成功打开。
如下:
利用mat分析内存泄漏
分析过程中,主要使用的是histogram直方图,和dominater tree支配树。
在histogram视图中查找retained heap值最大的项,并分析这里是否发生内存泄漏。
注意,一般情况下我们忽略java、android系统自带的对象,而着重分析我们自己程序中的对象。所以在上面输入过滤class name。
retained heap表示因为这个对象,会导致多少对象无法回收。
右击相应类,list objects->with incoming references。表明引用这个类的某个实例的其它类,也就是它在引用树中的父节点。通过分析该对象被谁引用,来判断为何没被垃圾回收。
outcoming reference就是子节点,查看一些当前对象引用着的对象。
此外看,merge shortest path to gc root,可以找到一条到gc root的最短路径,来看为什么当前对象无法被回收。
实战分析
下面记录了本人对一个项目的具体分析过程,以及各个工具的使用方法。
1、使用ddms查看内存
使用ddms的过程中,针对应用分别进行了多次检测,主要查看程序运行前的内存使用情况和程序运行后的内存使用情况:
使用前:
使用后:
通过上述数据可以看到,在程序运行前data object也就是在堆上分配的数据是180kb左右,而运行后内存大概在300kb上下浮动,没有呈现一个明显的一直上升的情况,故而没有明显的内存泄漏,基本没有导致oom的可能。
但是,可以发现,程序运行一次以后,放置一段时间,即便手动触发gc,堆上的内存虽然回落,但是仍然是288kb,与执行前的180kb相差较大,说明有一些对象被gc roots引用,无法完成释放。
下面采用mat工具进行进一步分析。在上面的过程中,转出了三个hprof文件,将hprof文件利用android sdk tools下的工具进行格式转换,进行对比分析:
2、使用mat分析内存转储
前面分析内存使用发现,使用前和使用后有一个100kb左右的差值,同时即便放置一段时间仍然无法使用。将before和after的直方图加入对比栏,在mat中进行对比:
点击右上角的红色叹号:
对比发现两个shallow heap大小基本相同,多出的部分是updatepartresultthread,系统类而不是我们自己编写程序造成的。
再看一下使用前后直方图中的retained heap:
可以看出,程序执行后,newactivity强引用了一些对象,在newacitivity没有推出前,retainedheap部分内存无法被回收。这也就是我们在ddms中发现堆内存差异的主要原因。
右击直方图中的newactivity,可以看见如下选项:
用的比较多的是list objects和merger shortest paths to gc roots。
list objects:
outgoing reference是支配树中当前对象的子节点,也就是当前对象持有哪些引用。
incoming reference是父节点,即当前对象被谁引用,为什么没被回收。
merger shortest paths to gc roots:找到当前无法被释放的对象到gc roots的最短路径。即排查当前对象被谁引用,为什么没有被释放。这里因为我们的对象是一个activity,当它显示在前台的时候,不会被垃圾回收,所以不是我们分析的点。
在这里,我们查看outgoing reference,查看当前对象拥有哪些强引用:
排除系统的对象,还是主要分析我们编写的程序。
最后发现,我们在之前使用leakcanary时,注册的相应监听器没有回收,发现了内存泄漏 :)。
去掉leakcanary,再次测试发现data object的值确实下降了不少。
继续分析,发现newactivity引用了一个
致使一部分内存无法被释放。这个问题属于客户端实现问题,不在内存泄漏的范围内。
接下来,在直方图中过滤出服务端的类:
可以看到,服务端的类大部分shallow heap都为0,也就是已经被垃圾回收。
结论
在使用mat分析内存时,最关键的就是找引用关系。如果一个应该被释放的对象没有被释放,那么我们往往要查看它的incoming reference,看看是谁持有了它的强引用。同时利用merger shortest gc roots找到到gc root的最短路径,确定是由于被谁引用而导致无法gc。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: struts2实现文件上传显示进度条效果
下一篇: Java掩码的几种使用例举