写点什么

一篇文章教你搞定内存泄漏与排查流程——安卓性能优化,20 道高频面试题(含答案)

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日

2、JAVA 回收机制




java 中是通过 GC(Garbage Collection)来进行回收内存,那 jvm 是如何确定一个对象能否被回收的呢?这里就需讲到其回收使用的算法

(1) 引用计数算法

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为 1。当任何其它变量被赋值为这个对象的引用时,计数加 1(a = b,则 b 引用的对象实例的计数器+1),当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减 1。任何引用计数器为 0 的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减 1。


优点:


引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。


缺点:


无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为 0。例如下面代码片段中,最后的 Object 实例已经不在我们的代码可控范围内,但其引用仍为 1,此时内存便产生泄漏


/举个例子/


Object o1 = new Object() //Object 的引用+1,此时计数器为 1


Object o2;


o2.o = o1; //Object 的引用+1,此时计数器为 2


o2 = null;


o1 = null; //Object 的引用-1,此时计数器为 1

(2) 可达性分析算法


可达性分析算法是现在 java 的主流方法,通过一系列的 GC ROOT 为起始点,从一个 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点(即图中的 ObjD、ObjE、ObjF)。由此可知,即时引用成环也不会导致泄漏。


java 中可作为 GC Root 的对象有:


1、方法区中静态属性引用的对象


2、方法区中常量引用的对象


3、本地方法栈 JNI 中引用的对象(Native 对象)


4、虚拟机栈(本地变量表)中正在运行使用的引用


但是,可达性分析算法中不可达的对象,也并非一定要被回收。当 GC 第一次扫过这些对象的时候,他们处于“死缓”的阶段。要真正执行死刑,至少需要经过两次标记过程。?如果对象经过可达性分析之后发现没有与 GC Roots 相关联的引用链,那他会被第一次标记,并经历一次筛选,这个对象的 finalize 方法会被执行。如果对象没有覆盖 finalize 或者已经被执行过了。虚拟机也不会去执行 finalize 方法。Finalize 是对象逃狱的最后一次机会。


3、四种引用




说到底,内存泄漏是因为引用的处理不正当导致的。所以,我们接下来需要老生常谈一下 java 中四种引用,即:强软弱虚(引用强度依次减弱)。


(1)强引用(Strong reference):?一般我们使用的都是强引用,例如:Object o = new Object();只要强引用还在,垃圾收集器就不会回收被引用的对象。


(2)软引用(Soft Reference):?用来定义一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要内存溢出之前,会将这些对象列入回收范围进行第二次回收,如果回收后还是内存不足,才会抛出内存溢出。(即在内存紧张时,会对其软引用回收)


(3)弱引用(Weak Reference):?用来描述非必须对象。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器回收时,无论内存是否足够,都会回收掉被弱引用关联的对象。(即 GC 扫过时,便将弱引用带走)


(4)虚引用(Phantom Reference):?也称为幽灵引用或者幻影引用,是最弱的引用关系。**一个对象的虚引用根本不影响其生存时间,也不能通过虚引用获得一个对象实例。**虚引用的唯一作用就是这个对象被 GC 时可以收到一条系统通知。


软引用与弱引用的抉择


如果只是想避免 OutOfMemory 异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。


4、小结




至此,我们知道内存泄漏是因为堆内存中的长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。


5、安卓内存泄漏排查工具




所谓工欲善其事必先利其器,这一小节先简述下所需借用到的内存泄漏排查工具,如果已经熟悉的话可以跳过。

(1) Android Profiler

这一工具是 Android Studio 自带,可以查看 cpu、内存使用、网络使用情况,Android Studio3.0 中用于替代 Android Monitor



① 强制执行垃圾收集事件的按钮。


② 捕获堆转储的按钮。


③ 记录内存分配的按钮。


④ 放大时间线的按钮。


⑤ 跳转到实时内存数据的按钮。


⑥ 事件时间线显示活动状态、用户输入事件和屏幕旋转事件。


⑦ 内存使用时间表,其中包括以下内容:


? 每个内存类别使用多少内存的堆栈图,如左边的 y 轴和顶部的颜色键所示。


? 虚线表示已分配对象的数量,如右侧 y 轴所示。


? 每个垃圾收集事件的图标。

(2) MAT(Memory Analyzer Tool)

MAT 用于锁定哪里泄漏。因为从 Android Profiler 中,知道了泄漏,但比较难锁定具体哪个地方导致了泄漏,所以借助 MAT 来锁定,具体使用待会会借助一个例子配合 Android Profiler 来介绍,稍安勿躁。


下载地址:[www.eclipse.org/mat/downloa…](


)


6、内存泄漏检查与解决流程




经过前面的一段理论,可能很多小伙伴都有些不耐烦了,现在便来真正的操作。


温馨提示:理论是进阶中必要的支持,否则只是知其然而不知其所以然


(1)第一步:对待检测功能扫雷式操作


当我们需要检查一块模块,或是整个 app 哪个地方有内存泄漏时,有时会比较茫然,有些大海捞针的感觉,毕竟泄漏不是每个页面都会有,而且有时是一个功能才会导致泄漏,所以我们可以采取“扫雷式操作”,也就是在需要检查的页面和功能中随便先使用一番,举个例子:假设检查 MainActivity 泄漏情况,可以登录进入后,此时来到了 MainActivity,后又登出,再次登录进入 MainActivity。


(2)第二步:借助 Android Profiler 获得内存快照


使用 Android Profiler 的 GC 功能,强制进行垃圾回收,再 dump 下内存("Android Profiler 功能简介"图的②按钮)。然后等待一段时间,会出现图中红色框部分:



在这里得到的页面,其实比较难直观获得内存分析的数据,最多只是选择“Arrange by package”按照包进行排序,然后进到自己的包下,查看应用内的 activity 的引用数是否正常,来判断其是否有正常回收



图中列的说明


Alloc Cout : 对象数


Shallow Size : 对象占用内存大小


Retained Set : 对象引用组占用内存大小(包含了这个对象引用的其他对象)


(3)第三步:借助 Android Studio 分析


至此,我们还是没得到直观的内存分析数据,我们需要借助更专业的工具。我们现将通过下图中红框内的按钮,将刚才的内存快照保存为 hprof 文件。



将保存好的 hprof 文件拖进 AS 中,勾选“Detect Leaked Activities”,然后点击绿色按钮进行分析。



如果有内存泄漏的话,会出现如下图的情况。图中很清晰的可以看到,这里出现了 MainActivity 的泄漏。并且观察到这个 MainActivity 可能不止一个对象存在,可能是我们上次退出程序的时候发生了泄漏,导致它不能回收。而在此打开 app,系统会创建新的 MainActivity。但至此我们只是知道 MainActivity 泄漏了,不知具体是哪里导致了 MainActivity 泄漏,所以需要借助 MAT 来进一步分析。



(4)第四步:hprof 文件转换


在使用 MAT 打开 hprof 文件前先要对刚才保存的 hprof 文件进行转换。通过终端,借助转换工具 hprof-conv(在 sdk/platform-tools/hprof-conv),使用命令行:


hprof-conv -z src dst


-z:排除不是 app 的内存,比如 Zygote


src:需要进行转换的 hprof 的文件路径


dst:转换后的文件路径(文件后缀还是.hprof)


(5)第五步:通过 MAT 进行具体分析?在 MAT 中打开转换了的 hprof 文件,如下图



打开后会看到如下图!


我们需要进入到"Histogram"来分析,点击下图中的按钮



打开"Histogram"后,会看到下图,在红框中输入在 AS 中观察到的泄漏的类,例如上面得知的 MainActivity



然后将搜索得到的结果进行合并,排除“软”、“弱”、“虚”引用对象,右键点击搜索到的结果,选择如下图的选项



得到合并结果如下



从分析结果可知,MainActivity 是因为 com.netease.nimlib.g.e 中的一个 hashMap 持有导致,这里的 e 类是第三方库的类,显然已被混淆,造成泄漏无非两种可能,一种是第三方库的 bug,一种是自己使用不当,例如忘记解绑操作等。具体的打断这个持有需要按照自己的代码进行分析,实例中的问题是因为使用第三方库注册后,在退出页面没有进行注销导致的。


当我们解决完后,可以再次进行一轮内存快照,直到没有内存泄漏,过程会比较枯燥,但一点点的解决泄漏最终会给 app 一个质的飞跃。


7、常见的内存泄漏原因




(1)集合类


集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。


(2)单例模式


不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被 JVM 正常回收,导致内存泄露。


public class SingleTest{


private static SingleTest instance;


private Context context;


private SingleTest(Context context){


this.context = context;


}


public static SingleTest getInstance(Context context){


if(instance != null){


instance = new SingleTest(context);


}


return instance;


}


}


这里如果传递 Activity 作为 Context 来获得单例对象,那么单例持有 Activity 的引用,导致 Activity 不能被释放。?不要直接对 Activity 进行直接引用作为成员变量,如果允许可以使用 Application。?如果不得不需要 Activity 作为 Context,可以使用弱引用 WeakReference,相同的,对于 Service 等其他有自己生命周期的对象来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。


(3)未关闭或释放资源


BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback 等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。值得注意的是,关闭的语句必须在 finally 中进行关闭,否则有可能因为异常未关闭资源,致使 activity 泄漏


(4)Handler


只要 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。特别是 handler 执行延迟任务。所以,Handler 的使用要尤为小心,否则将很容易导致内存泄露的发生。


public class MainActivity extends AppCompatActivity {


private Handler mHandler = new Handler(){


@Override


public void handleMessage(Message ms


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


g) {


//do something


}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
一篇文章教你搞定内存泄漏与排查流程——安卓性能优化,20道高频面试题(含答案)