前言
先回顾下内存泄露的定义:在计算机运行过程中,如果有对象超出了预期的生命周期继续存活在内存中,导致这部分内存不能正常地回收和重新利用,我们就说发生了内存泄露。
每个对象被创造出来都是有目的的,它为了完成某个任务而生,当完成它的任务后,对象就会跟任务链上的其他对象断开联系,等待最后的终结-销毁。因此我们可以根据这个对象和其他对象的链接情况来判断此对象是否超过它自己的生命周期。目前有两种方案:
1)引用计数法
2)可达性分析
前者是根据是否被其他对象引用,后者是根据是否被重要对象(GC ROOT)间接或者直接引用,他们都是通过联接情况来判断对象是否应该被销毁。
没有联接的对象应该销毁,但有联接的对象可能也是需要销毁的,所以我们根据联接判断,会出现“关系户”逃脱生死审判,从而产生内存泄露。
当系统无能为力的时候,那么程序员便要站出来了,他们需要每个对象进行审查,弄清楚这个对象什么情况下什么时候应该被销毁,然后在这个时刻去详细观察内存,看看这个时候它是否真的已经被销毁,如果没有就是发生了内存泄露,这就是检测内存泄露的基本思路。
本文就Android系统中内存检测的常见手段进行分析,在分析过程中,请思考如下问题:
1)每种检测手段是否检测了所有对象,重点检测哪些对象,为什么呢;
2)每种检测手段定位内存泄露的方法;
3)检测手段未来如何发展。
Android系统的内存泄露检测
现如今Android系统常见的内存泄露检测手段(工具)有三个:
1 Memory Profiler
2 Memory Analyzer
3 LeakCanary
接下来就主要介绍每种工具如何使用以及他们的原理。
Memory Profiler
profile: 剖面、 侧面,Memory Profiler :内存剖析者,它是 Android Profiler中重要组件,可帮助分析应用卡顿、内存泄漏等原因。
Memory Profiler使用方法
一、找到Android Profiler
二、连接上手机或者打开模拟器
三、选择Profiler里面的Memory
可以看到Memory Profiler是利用实时图表记录应用内存使用情况的。用户的每一项操作(如打开新页面)都会伴随着进程中某些对象的生成和销毁,从而导致进程占用的内存发生变化,如果这个变化与预期的内存变化不一致,我们就能猜测到可能发生了内存泄露。
这里就这图中主要按钮功能进行简单介绍:
1号按钮:这个带有垃圾桶图标的按钮是强制垃圾回收(GC)按钮;
2号按钮:这个带有一个向下箭头的按钮是堆转储按钮,就是把当前内存堆中的对象全部倾倒(dump)出来,放在手机SD卡等存储设备中,形成一个.hprof文件,通过查看这个文件,我们可以看到内存中有哪些对象。
3号按钮: Record按钮 或者叫 Allocation Tracking按钮,这个按钮可以记录一段时间内存分配情况。但是注意这个只有在Android 7.1以下版本可以使用。为了在分析时提高应用性能,Memory Profiler 在默认情况下会定期对内存分配进行采样。在运行 API 级别 26 或更高级别的设备上进行测试时,您可以使用 Allocation Tracking 下拉菜单来更改此行为。可用选项如下:Full:捕获内存中的所有对象分配。这是 Android Studio 3.2 及更低版本中的默认行为。如果您有一个分配了大量对象的应用,则可能会在分析时观察到应用的运行速度明显减慢。Sampled:定期对内存中的对象分配进行采样。这是默认选项,在分析时对应用性能的影响较小。在短时间内分配大量对象的应用仍可能会表现出明显的速度减慢。None:停止跟踪应用的内存分配。
4号按钮: 暂停或者继续抓取内存快照。
5号线:某一个时刻点应用占用内存情况。
四、Memory Profiler的使用
从上面的Profiler主页介绍可以看出来Memory Profiler以实时图标形式展示内存使用情况,提供强制GC和堆转储的能力,在低版本手机上还可以跟踪内存分配情况。利用这些能力我们可以初步定位内存泄露,以及发现一些简单的内存泄露,具体使用如下:
1)对于每一个应用来说,内存占多少是有一定的规律的,比如打开一个页面内存占用就会增加,打开一张图片内存占用也会增加,退出一个页面内存占用就应该减少等,我们可以根据这些预期内存占用曲线和实际内存占用曲线是否一致来判断是否发生了内存泄露。比如我们反复打开关闭一个Activity,但是发现实际内存占用曲线是一直异常上升,便可以初步判断发生了内存泄露,接下来就要重点关注这个Activity。
2)在特定时刻,先GC,再Dump heap就可以获取当前进程内存堆情况,我们可以看内存中对象是否和我们预期一样,比如我们用filter 筛选出我们重点关注的Activity,如下图:
点击LeakDemoActivity(我们自己制造的内存泄露Activity),右侧出现它的实例,我们发现它有四个实例,再点击实例发现每个实例的引用链,在右下角引用链LeakDemoActivity,可以发现这个实例是被一个ArrayList锁定的,我们直接点击LeakDemoActivity去寻找列表,然后发现:
public class LeakDemoActivity extends BaseActivity {
public final static ArrayList mContextList=new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_demo);
mContextList.add(this);
}
}
很显然问题就在这个mContextList。
Memory Profiler监测内存泄露原理
1:每隔一段时间对目标进程进行采样形成内存占用的实时图表。
这里存在一个问题,如何获取当前进程内存使用情况呢?
答:主流的JVM都提供了一套API-JVMTI(JVM Tool Interface),利用这套API我们可以获取进程很多监控数据,包括内存使用情况,cpu使用情况等。Memory Profiler命令会使进程启动时候同时启动一个伴生的库(运行在同一个进程内),这个库启动后会调用JVMT去采样进程数据,然后缓存-发送给我们的观测者-android Memory Profiler。
2 手动dump内存某一个时刻点的具体分配情况,来进一步分析,一般有三种主要分析方法:
A 通过熟悉业务流程去理清某个对象的生命周期,然后可以通过内存快照看特定时刻这个对象是否和预期一样生或者被销毁;
B 通过对比不同时刻点内存快照,发现此段时间内新分配的对象,看是否跟对象生命周期表现一致;
C 把dump来的“.hprof”文件放到MAT工具进行进一步的分析,比如find path to GC ROOT。
Memory Analyzer
Memory Analyzer使用方法
MAT主要是提供对.hprof文件的静态分析,是Memory profiler静态分析部分的增强版,它能够从多个维度对内存快照文件(.hprof)进行分析。下面介绍仅介绍下MAT在检测内存泄露方面的一些基础用法。
如上图是MAT打开一个.hprof文件的首页,下面的Histogram、Leak_Suspects是从不同角度对该文件进行分析得出的一个结果。这里简单介绍两种用MAT寻找内存泄露的角度:
1)点击Leak_suspects,这个是MAT自动分析的得出的结果,大致看了下这个仅仅是MAT列举出来占用内存比较多的类,它不一定会造成内存泄露,对我们定位内存问题具有一定的指导意义,同时Leak_suspects还给出了从系统层面的一些概述-比如线程,大的对象等。
2)点击Histogram(直方图),进入直方图角度观测内存,直方图作用是列举出来每个类的实例,这样我们就可以在快照时刻看类实例的个数是否是正确的,如果不正确,我们还可以查找每个实例到GC Root的引用链,从而确定是否内存泄露。具体可以参考如下步骤,点击Histogram进入直方图主页,搜索我们关注的对象比如MainActivity->右键选择within ref->右键 path to GC root 就可以看到找到我们关注的对象为啥没有被回收的原因。
Memory Analyzer原理
Memory Analyzer是一个文件分析工具,他可通过分析.hprof文件给出跟内存相关的诸多信息。这跟一个班主任从学生成绩表中统计出全班平均分,各科平均分一样的工作是差不多的。
.hprof是什么呢? 它是一种文件格式。文件格式呢, 不过是一种数据的包装,可以使数的据存储、 运输、 使用更加安全、方便。说多了哈,.hprof 是我们利用Android studio dump(倾到)出来的,对,这个文件就是as接收到我们点击命令后指挥手机系统把当前的cpu状态,当前内存使用情况、当前进程的所有对象序列化之后存储起来形成的一个文件。序列化是什么呢?简答来说序列化就是把一个对象和它当前的状态存储起来,同时也把跟这个对象有连接的对象序列化。
.hprof有了内存状态,有了cpu状态,有了每个对象内存占用和状态以及它跟谁有链接,我们自己也可以人工统计出哪些对象占内存多,哪些对象跟GC root对象有关联,哪些对象可能存在内存泄露,但是一个.hprof文件包含数据非常多,人工统计何其耗时,所以MAT就出现了,它通过执行一些规则,自动帮我们进行第一步的分析,给出我们统计表格,内存泄露的对象,大大节省了我们分析的时间。
LeakCanary
LeakCanary使用方法
略,请百度,也可以去:https://github.com/square/leakcanary 找使用方法。
LeakCanary原理
我们看到LeakCanary主要做了三件事:
一 、定位内存泄露的对象--哪个Activity或者Fragment;
二、 指出内存泄露的原因--对象被GC ROOT引用的路径;
三、 在通知栏展示该原因(略,自己百度);
其实它的原理也是包含两部分的,首先我们看第一部分:
如何定位内存泄露的对象?其实在本文的开始已经给出检测特定对象内存泄露的基本原理:步骤一、弄清这个对象的生命周期,知道它再哪个时刻应该被销毁;步骤二、 在这个时刻去观察这个对象是否真的已经被销毁,如果没有就是发生了内存泄露。具体来说就是:
1 hook 特定类的生命周期, 比如Activity或者Fragment;
2 构建一个弱引用ObjectWatcher,弱引用上述类的实例;
3 当某个Activity或者Fragment onDestory()一小段时间后(5s),去检查ObjectWatcher对应的弱引用是否还在,如果在执行一次GC,再次检查,如果这时候依旧在,证明该Activity或者Fragment发生了内存泄露。
如何指出内存泄露的原因呢,其实就是找到该对象的最短的Path to GC Root呢?这里就是上面两个工具的合体了:
1 调用JVM对应的api dump内存.hprof,这一步就相当于给内存照个照片存在手机存储系统里;
2 检索.hprof文件, 找到上一步定位到泄露对象的path to GC Root。
至此,三种方案都介绍完毕 ,接下来我们探讨下未来还有没有更好的方案。
未来方案探讨
研究这些检测工具的原理后,我们发现检测内存泄露的关键在于三点:
一、知道哪些对象可能发生泄漏;
二、可以清晰知道这些对象在何时销毁,比如有关于这个销毁时间点的状态标志、监听回调等;
三、在对象销毁应该销毁时刻(通过监听、状态标志等确认)通过各种方案去检查这些对象是否销毁,如没有,则给出Path to GC Root。
其中第三点是有通用解决方案的,所以比较难搞定的就是前两点。
如何知道哪些对象可能发生内存泄露呢?
已有方案就是列举出来占用内存比较大的对象,这种想法很直接也的确有效果,但也会有无用分析太多,比如占用内存稍微小的对象太多没有时间一个个分析等缺陷,这是一种事前预估的方案。
因为检测内存泄露不是一次就能搞定的,也不是只有一个app出现的问题,所以可以用一种动态长远的眼光和整体的角度看待这个问题,如果能够通过一种方便的方式(平台活着集成sdk)来统计内存泄露,那么就可以在某一个整体级别比如app级别、公司级别、平台级别,通过统计或者机器学习给出内存泄露最可能发生的对象、对象类别,场景信息等,这是一种经历史经验模式。
如何定位内存泄露时刻呢?
现在方案要么是已经很确切的知道一些对象的生命周期,比如Activity;要么就是看业务代码确认生命周期,再人工分析内存快照快照确认,前一种只有有限的几种,后一种纯人工分析一次要很久。
所以我能想到的改进莫过于 1 提供一个简单的输入可以把已知的生命周期录入,然后实现自动分析 2 同样通过统计或者机器学习方式提供一些怀疑对象类别的生命周期。
您还有什么想法么,评论区见。