宝剑锋从磨砺出,梅花香自苦寒来。这篇文章主要讲述Android内存泄漏检测与MAT使用相关的知识,希望能为你提供帮助。
公司相关项目需要进行内存优化,
所以整理了一些分析内存泄漏的知识以及工作分析过程。
本文中不会刻意的编写一个内存泄漏的程序,
然后利用工具去分析它。而是通过介绍相关概念,
来分析如何寻找内存泄漏,
并附上自己的项目实战过程。
撰写过程中,
本人深感JVM、操作系统相关知识了解不够深刻,
不足之处非常欢迎指正说明。
内存泄漏基本概念 内存检测这部分,
相关的知识有JVM虚拟机垃圾收集机制,
类加载机制,
内存模型,
以及操作系统的基础知识(
所以不要说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,
同时可以在相应程序查看哪里发生内存泄漏。
方法比较简单,
具体步骤可以查阅官方github。添加leakcanary依赖以后,
新建一个Application入口,
在Oncreate方法中安装Leakcanary即可。
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A551H02-0.jpg)
文章图片
当发生内存泄漏时, 屏幕会出现Toast, 同时打开桌面上的Leaks程序, 显示泄漏的内存, 如下图:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5515012-1.jpg)
文章图片
整体流程
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是否发生内存泄漏。 对于一些项目比如sdk开发, 很可能整个程序没有一个Activity, 所以这种方式就不是很实用。
观看整体内存使用情况 详情参见官方文档:
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的分配情况。
结果如下:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5511943-2.jpg)
文章图片
分析各个参数:
Private Clean/Dirty RAM:
这部分内存是app的私有内存, 当app销毁是操作系统可以回收到全部的内存。其中private dirty只能被你的进程使用, 同时只能存在在内存当中, 当内存不够, 也不能通过分页技术存储到硬盘( 操作系统相关知识) , dalvik和native heap上的分配都是private dirty RAM。 Dirty RAM是内存中被修改过的页面, 而Clean RAM是从持久文件( 比如代码执行文件) 映射出的内存。
PSS Total:
我们知道, 进程之间彼此通信底层通过Binder Driver, 通过操控一块共享内存进行读写来相互通信。这样一来, 为了进程间通信, Binder会为每个进程在共享内存中开辟一块空间。
PSS的部分, 包含了每个进程的共享内存。例如, 一个内存页面被两个进程共享, 那么页面大小的一半会被加到两个进程各自的PSS中。
通过累加全部进程的PSS, 我们可以查看整个系统的内存使用情况。事实上, PSS是衡量 ( 实际) 使用内存的重要标准。
Dalvik Heap:
该字段衡量的是Dalvik虚拟机上堆分配情况, 也就是我们在Java中使用new生命对象分配的内存。
列中PSS Total包括了和其它Zygote进程共享的内存( 全部app进程都是从Zygote中fork出来的, 都有一部分内存共享) 。而Private Dirty则是app进程本身所使用的的内存。
.so mmap / .dex mmap
这部分主要指的是本地代码( so) 和Davlik 虚拟机代码( dex) 的代码大小。PSS Total列中指的是包含android平台的代码, 而private clean仅仅是程序本身运行的代码。
上面参数很多, 理解相关知识需要掌握操作系统内存部分。我们在测试的使用, 一般情况下, 我们关注private Dirty或者pss Total就可以查看app内存整体趋势。
DDMS 使用流程
- 启动eclipse后, 切换到DDMS透视图, 并确认Devices视图、Heap视图都是打开的;
- 将手机通过USB链接至电脑, 链接时需要确认手机是处于“USB调试”模式, 而不是作为“MassStorage”;
- 链接成功后, 在DDMS的Devices视图中将会显示手机设备的序列号, 以及设备中正在运行的部分进程信息;
- 点击选中想要监测的进程, 比如system_process进程;
- 点击选中Devices视图界面中最上方一排图标中的“Update Heap”图标;
- 点击Heap视图中的“Cause GC”按钮;
- 此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况。
正常情况下Total Size值都会稳定在一个有限的范围内, 也就是说没有造成对象不被垃圾回收的情况, 所以说虽然我们不断的操作会不断的生成很多对象, 而在虚拟机不断的进行GC的过程中, 这些对象都被回收了, 内存占用量会会落到一个稳定的水平。如果代码中存在没有释放对象引用的情况, 则dataobject的Total Size值在每次GC后不会有明显的回落, 随着操作次数的增多Total Size的值会越来越大
通过DDMS方式, DataObject 的totalSize如果稳定在一个大概范围内, 则可以确定没有发生内存泄漏。
MAT 然而, 并不是所有的内存泄漏都十分明显, 并且会最终导致OOM。有时候只有几个对象被泄漏, 虽然影响不大, 但是无疑浪费了内存。
要发现这种比较隐蔽的内存泄漏, 我们需要使用MAT工具。
在了解MAT具体使用之前, 要先了解一些相关概念。
支配树 支配树体现了对象实例间的支配关系, 在对象引用图中, 所有指向对象B的路径都经过对象A, 则认为对象A支配对象B。
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A551D09-3.jpg)
文章图片
在这张图里, 左边是对象引用关系, 对于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直接或间接引用的对象。他们暂时不会被回收。如下图:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A55121V-4.jpg)
文章图片
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进行内存分析经常使用的。
我们在分析内存泄漏的时候, 着重会查看retained heap, 也就是这个对象没有被释放前, retained heap中的相关内存不会被释放。
然后, 在分析某个对象为何没被释放的时候, 会查看引用关系或者支撑树。因为引用树父子关系可能比较杂乱, 而支撑树更加清晰。
在使用MAT分析内存泄漏的过程中, 主要流程就是:
1、分析retained heap, 找一个使很多对象无法被释放的内存。
2、正常情况下, 该释放这个对象, 所以通过支撑树, 或者查看GC 路径, 分析为什么这个对象没有被释放。
MAT的下载与使用 下载地址: https://eclipse.org/mat/downloads.php
这里没有作为eclipse插件的方式下载mat, 而是通过下载单独的软件客户端。
首先, 在DDMS中选择要检测的进程并dump HPROF file, 如下图:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5512493-5.jpg)
文章图片
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中打开, 注意要转换格式, 不然无法成功打开。
如下:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5511B8-6.jpg)
文章图片
利用MAT分析内存泄漏 分析过程中, 主要使用的是Histogram直方图, 和Dominater tree支配树。
在Histogram视图中查找retained heap值最大的项, 并分析这里是否发生内存泄漏。
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A55150M-7.jpg)
文章图片
上图中一坨一坨的, 其实就是Class的名称。这样分类比较清晰, 后面会说到如何查看Class声明的对象。
在最上面class Name下有输入过滤的地方, 需要注意是, 如果要查看com包下的类, 那么要输入com. , 这里的正则中’*’貌似不会去匹配’.’, 所以就要我们自己输入啦。 一般情况下, 我们忽略会java、android系统自带的类, 而着重分析我们自己程序中编写的对象内存使用情况。
Retained heap表示因为这个对象, 会导致多少对象无法回收。
右击相应类, list objects-> with incoming references。表明引用这个类的某个实例的其它类, 也就是它在引用树中的父节点。通过分析该对象被谁引用, 来判断为何没被垃圾回收。
outcoming reference就是子节点, 查看一些当前对象引用着的对象。
此外看, Merge shortest path to gc root, 可以找到一条到GC root的最短路径, 来看为什么当前对象无法被回收。
实战分析 下面记录了本人对一个项目的具体分析过程, 以及各个工具的使用方法。
1、使用DDMS查看内存
使用DDMS的过程中, 针对应用分别进行了多次检测, 主要查看程序运行前的内存使用情况和程序运行后的内存使用情况:
使用前:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A551A26-8.jpg)
文章图片
使用后:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5515007-9.jpg)
文章图片
通过上述数据可以看到, 在程序运行前data object也就是在堆上分配的数据是180KB左右, 而运行后内存大概在300KB上下浮动, 没有呈现一个明显的一直上升的情况, 故而没有明显的内存泄漏, 基本没有导致OOM的可能。
但是, 可以发现, 程序运行一次以后, 放置一段时间, 即便手动触发GC, 堆上的内存虽然回落, 但是仍然是288KB, 与执行前的180KB相差较大, 说明有一些对象被GC roots引用, 无法完成释放。
下面采用MAT工具进行进一步分析。在上面的过程中, 转出了三个hprof文件, 将hprof文件利用Android sdk tools下的工具进行格式转换,进行对比分析:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5514336-10.jpg)
文章图片
2、使用MAT分析内存转储
前面分析内存使用发现, 使用前和使用后有一个100KB左右的差值, 同时即便放置一段时间仍然无法使用。将before和after的直方图加入对比栏, 在MAT中进行对比:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5514235-11.jpg)
文章图片
点击右上角的红色叹号:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A55140A-12.jpg)
文章图片
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A551JP-13.jpg)
文章图片
对比发现两个shallow heap大小基本相同, 多出的部分是UpdatePartResultThread, 系统类而不是我们自己编写程序造成的。
再看一下使用前后直方图中的retained heap:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5516407-14.jpg)
文章图片
可以看出, 程序执行后, newActivity强引用了一些对象, 在newAcitivity没有推出前, retainedheap部分内存无法被回收。这也就是我们在DDMS中发现堆内存差异的主要原因。
右击直方图中的NewActivity, 可以看见如下选项:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A551NE-15.jpg)
文章图片
用的比较多的是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, 查看当前对象拥有哪些强引用:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A55164D-16.jpg)
文章图片
排除系统的对象, 还是主要分析我们编写的程序。
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5515936-17.jpg)
文章图片
最后发现, 我们在之前使用LeakCanary时, 注册的相应监听器没有回收, 发现了内存泄漏 :) 。
去掉LeakCanary, 再次测试发现data object的值确实下降了不少。
继续分析, 发现newActivity引用了一个
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A55110C-18.jpg)
文章图片
致使一部分内存无法被释放。这个问题属于客户端实现问题, 不在内存泄漏的范围内。
接下来, 在直方图中过滤出服务端的类:
![Android内存泄漏检测与MAT使用](http://img.readke.com/220415/0A5512249-19.jpg)
文章图片
可以看到, 服务端的类大部分shallow heap都为0, 也就是已经被垃圾回收。
结论 【Android内存泄漏检测与MAT使用】在使用MAT分析内存时, 最关键的就是找引用关系。如果一个应该被释放的对象没有被释放, 那么我们往往要查看它的incoming reference, 看看是谁持有了它的强引用。同时利用Merger shortest GC roots找到到GC root的最短路径, 确定是由于被谁引用而导致无法GC。
推荐阅读
- Android 网络图片加载缓存处理库ImageLoader和Picasso
- Android中LaunchMode详解
- Android:内存优化的一些总结
- Android Activity简介和自定义视图
- Android使用Application的好处
- android ProgressBar 进度条的进度两端是圆角的方法
- Android之文件数据存储
- android 和h5互调步骤
- Android中实现多彩的霓虹灯