内存泄漏与排查流程——安卓性能优化

发表于:2018-3-29 11:24

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:猛猛的小盆友    来源:51Testing软件测试网采编

  前言
  内存泄漏可以说是安卓开发中常遇到的问题,追溯和排查其问题根源是进阶的程序猿必须具备的一项技能。小盆友今天便与大家分享一下这方面的一些见解,如有理解错误或是不同见解,可以于评论区留言我们进行讨论,如果喜欢给个赞鼓励下吧。
  篇幅较长,可以通过目录寻找自己所需了解的吧
  目录
  1、JAVA内存解析
  2、JAVA回收机制
  3、四种引用
  4、小结
  5、安卓内存泄漏排查工具
  6、内存泄漏检查与解决流程
  7、常见的内存泄漏原因
  1、JAVA内存解析
  要想知道内存泄漏,需要先了解java中运行时内存是怎么构成的,才能知道是哪个地方导致。话不多说,先上图
  java内存模型
  运行时的java内存分为两大块:线程私有(蓝色区域)、共享数据区(黄色区域)
  线程私有:主要用于存储各个线程私有的一些信息,包括:程序计数器、虚拟机栈、本地方法栈
  共享数据区:主要用于存储公用的一些信息,包括:方法区(内含常量池)、堆
  程序计数器:让程序中各个线程知道自己接下来需要执行哪一行。在java中多线程为抢占式(因为cpu在某一时刻只会执行一条线程),当线程切换时,需要继续哪一行便由程序计数器告知。
  举个例子:A、B两条线程,此时CPU执行从A切换至B,过了段时间从B切换回A,此时A需要从上次暂停的地方继续执行,此时从哪一行执行就是由程序计数器来提供。
  值得一提:
  (1)若执行java函数时,程序计数器记录的是虚拟机字节码的地址;
  (2)若执行native方法时,程序计数器便置为了null。
  (3)在java虚拟机规范中,程序计数器是唯一没有定义OutOfMemoryError。
  虚拟机栈:描述的是java方法的内存模型,平时说的“栈”其实就是虚拟机栈,其生命周期与线程相同。每个方法(不包含native方法)执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  值得一提:在java虚拟机规范中,此处定义了两个异常
  (1)StackOverFlowError (在递归中常看到,递归层级过深)
  (2)OutOfMemoryError
  本地方法栈:是为虚拟机使用到的Native方法提供内存空间。有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,比如主流的HotSpot虚拟机。
  值得一提:在java虚拟机规范中,此处定义了两个异常
  (1)StackOverFlowError (在递归中常看到,递归层级过深)
  (2)OutOfMemoryError
  方法区:主要存储已加载是类信息(由ClassLoader加载)、常量、静态变量、编译后的代码的一些信息。GC在这里比较少出现在这块区域。
  堆:存放的是几乎所有的对象实例和数组数据。是虚拟机管理的最大的一块内存,是GC的主战场,所以也叫“GC堆”、“垃圾堆” 。
  值得一提:在java虚拟机规范中,此处定义了一个异常
  (1)OutOfMemoryError
  运行时常量池:属于“方法区”的一部分,用于存放编译器生成的各种字面量和符号引用。
  字面量:与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。
  符号引用:编译语言层面的概念,包括以下3类:
  (1) 类和接口的全限定名
  (2)字段的名称和描述符
  (3)方法的名称和描述符
  2、JAVA回收机制
  java中是通过GC(Garbage Collection)来进行回收内存,那jvm是如何确定一个对象能否被回收的呢?这里就需讲到其回收使用的算法
  引用计数算法
  引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为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
  可达性分析算法
  可达性分析算法
  可达性分析算法是现在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
  Android Profiler功能简介
  ① 强制执行垃圾收集事件的按钮。
  ② 捕获堆转储的按钮。
  ③ 记录内存分配的按钮。
  ④ 放大时间线的按钮。
  ⑤ 跳转到实时内存数据的按钮。
  ⑥ 事件时间线显示活动状态、用户输入事件和屏幕旋转事件。
  ⑦ 内存使用时间表,其中包括以下内容:
  ● 每个内存类别使用多少内存的堆栈图,如左边的y轴和顶部的颜色键所示。
  ● 虚线表示已分配对象的数量,如右侧y轴所示。
  ● 每个垃圾收集事件的图标。
  (2)MAT(Memory Analyzer Tool)
  MAT用于锁定哪里泄漏。因为从Android Profiler中,知道了泄漏,但比较难锁定具体哪个地方导致了泄漏,所以借助MAT来锁定,具体使用待会会借助一个例子配合Android Profiler来介绍,稍安勿躁。
  下载地址:http://www.eclipse.org/mat/downloads.php
  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)




上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号