一篇文章教你搞定内存泄漏与排查流程——安卓性能优化

值得一提:在java虚拟机规范中,此处定义了一个异常

(1)OutOfMemoryError

  1. 运行时常量池:属于“方法区”的一部分,用于存放编译器生成的各种字面量和符号引用。

字面量:与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。

符号引用:编译语言层面的概念,包括以下3类:

(1) 类和接口的全限定名

(2)字段的名称和描述符

(3)方法的名称和描述符

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 msg) {

//do something

}

};

private void loadData(){

//do request

Message message = Message.obtain();

mHandler.sendMessage(message);

}

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-s4UccGex-1712691835600)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值