内存泄露从入门到精通三部曲之排查方法篇
最原始的内存泄露测试
重复多次操作关键的可疑的路径,从内存监控工具中观察内存曲线,是否存在不断上升的趋势且不会在程序返回时明显回落。 这种方式可以发现最基本,也是最明显的内存泄露问题,对用户价值最大,操作难度小,性价比极高。
最简单有效的泄露测试
分模块测试,进入一个模块然后操作所有的操作,遍历所有的路径,然后退出这个模块,执行一次GC,然后dump出具体的内容,分析与这模块相关的Activity,Fragment还有相关的类还存不存在,然后看具体的引用路径。
分析内存泄漏具体流程
先利其器 -- HPROF Viewer and Analyzer
A区域:
Total Count:表示该类有多个个对象实例。
Heap Count:表示堆内存中该类有多少个实例,该类的实例有可能在栈中,例如方法内的局部变量。
Sizeof:对象本身占用内存的大小,不包含其引用的对象,但它只是一个对象实例的大小。也就是一个对象实例的真实大小,它不表示占用了内存,要想知道它占用多少内存要看下面的数据,它只是一个说明书,表示它需要75K大小,但是内存中有没有这个对象,有几个这就另说。
ShallowSize:对象本身占用内存的大小,不包含其引用的对象。它的大小 = TotalCount * SizeOf 表示内存中共有多少个这个对象的实例,SizeOf表示一个对象占用的大小,它们相等就是所有占用的大小,且记,它只包含自己本身的大小,不包含引用对象的大小,例如它有一个引用,它只占这个对象的4byte,并不会把这个引用所引用的内容算上。
RetainedSize:对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。所以,有人说,这个值是当这个对象被释放后所能释放的总大小,其实不太准确,因为如果它引用一些单例这些对象并不会被释放。所以这个值通常会比ShallowSize大。
B区域
Depth:The shortest number of hops from any GC root to the selected instance.
ShallowSize:对象本身占用内存的大小,不包含其引用的对象,因为这里是把一个对象一个对象列出的,所以它就是一个对象实例的大小。
Dominating Size:表示自己的ShallowSize+自己直接或者间接访问对象的ShallowSize总大小 。你发现这两个Dominating Size相加正好与A区域的RetainedSize相等。
C区域与B区域相同。
如果你在生成hprof文件后,点击“Analyzer Tasks”可以替你分析LeakedActivity和Duplicate Strings。
参考:https://developer.android.com/studio/profile/am-hprof.html
这个工具主要看在GC之后该回收的有没有被回收,如果它应该被回收但是heapCount不为0,说明堆中有数据啊,那么就应该怀疑它,导出这个hprof文件,然后用MAT分析它。
先利其器 -- MAT
Dump出内存泄露当时的内存镜像hprof,分析怀疑泄露的类。
list objects -- with outgoing references : 查看谁引用了这个对象,当然引用可能是在这个类的外部,也可能是在类的内部。 同理,with incoming references就是它引用了谁。
看看所引用目标对象的最短GC路径,为什么排队了三种引用,因为我们强GC过了。
这个图是最关键的,从Mat的Inspector中我们可以看到,最顶的是GCRoot,然后它的成员变量mMap,其类型为java.util.HashMap。然后再来看mHap的值,table是它的数据结构,HashMapEntry是它的实体,value是这个实体对应的一个值,这个value的类型是DownloadTask,而DownloadTask里有一人mContext变量,它的类型是ToolsMarketActivity。左边的是变量名称,这个变量名称是父节点内的变量,右边是这个变量的类型,掌握好这点就可以看懂整个GC最短引用树了。
看代码找到真正泄露的根源所在~~~
先利其器 -- MAT 2
MAT对比操作前后的hprof来定位内存泄露的根因所在。 为查找内存泄漏,通常需要两个 Dump结果作对比,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去 A)第一个HPROF 文件(usingFile > Open Heap Dump ). B)打开Histogram view. C)在NavigationHistory view里 (如果看不到就从Window >show view>MAT- Navigation History ), 右击histogram然后选择Add to Compare Basket . D)打开第二个HPROF 文件然后重做步骤2和3. E)切换到Compare Basket view, 然后点击Compare the Results (视图右上角的红色"!"图标)。
可以看出两个hprof的数据对象对比结果。 通过这种方式可以快速定位到操作前后所持有的对象增量,从而进一步定位出当前操作导致内存泄露的具体原因是泄露了什么数据对象。
先利其器 -- System Information
在之前版本中,这个功能是靠adb dump这些命令来执行的,在新版里就直接以工具化形式展示出来了。
先利其器 -- Allocation Tracking
先利其器 -- LeakCanary
先利其器 -- Lint
Lint是在Eclipse时代的一个产物,不过在AS上更加好用。
在AS中,Analyze选项中,选择InspectCode进行代码静态分析。
这个工具被Google搞得特别有用。左边是问题,右边是这个问题的描述及其修复方案。
能把静态代码分析给处理一下,会对代码代码上有一个飞跃!!!
Analyze选项下,所有的都特别重要,有必要开一个Lint的使用大全。
Lint也可检查布局是否存在可优化的地方,其实已经在静态代码优化里面了。Lint的如下规则是专门为优化布局设置的:
AndroidLintUseCompoundDrawables
内存泄露监控方案
这个内存泄露检测的基本原理是:
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列(ReferenceQueue)联合使用(在虚引用函数就必须关联指定)。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,自动把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
基于以上原理,MLD工具在调用接口addObject加入监控类型时,会为该类型对象增加一个虚引用,注意虚引用并不会影响该对象被正常回收。因此可以在ReferenceQueue引用队列中统计未被回收的监控对象是否超过指定阀值。
利用PhantomReferences(虚引用)和ReferenceQueue(引用队列),当PhantomReferences被加入到相关联的ReferenceQueue时,则视该对象已经或处于垃圾回收器回收阶段了。
MLD监控原理核心
目前手机管家已对大部分类完成内存泄露的监控,包括各种activity,service和view页面等,务求在技术上能带给用户最顺滑的产品体验。
接下来简单介绍下这个工具的判断核心。根据虚引用监控到的内存状态,需要通过多种策略来判断是否存在内存泄露。 (1)最简单的方式就是直接在加入监控时就为该类型设定最大存在个数,举个例子,各个DAO对象理论上只能存在最多一个,因此一旦出现两个相同的DAO,那一般都是泄露了; (2)第二种情况是在页面退出程序退出时,检索gc后无法释放的对象列表,这些对象类型也会成为内存泄露的怀疑对象; (3)最后一种情况比较复杂,基本原理是根据历史操作判断对象数量的增长幅度。根据对象的增长通过最小二乘法拟合出该对象类型的增长速度,如果超过经验值则会列入疑似泄露的对象列表。
3.3 UIAutomator完成重复操作的自动化 最后一步就很简单了。这么多反复的UI操作,让人工来点就太浪费人力了。我们使用UIAutomator来进行自动化操作测试。 目前手机管家的每日自动化测试已覆盖各个功能的主路径,并通过配置文件的方式来灵活驱动用例的增删改查,最大限度保证了随着版本推移用例的复用价值。
Last updated