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

前言

内存泄漏能够说是安卓开发中常遇到的问题,追溯和排查其问题根源是进阶的程序猿必须具有的一项技能。小盆友今天便与你们分享一下这方面的一些看法,若有理解错误或是不一样看法,能够于评论区留言咱们进行讨论,若是喜欢给个赞鼓励下吧。php

篇幅较长,能够经过目录寻找本身所需了解的吧java

目录

一、JAVA内存解析
二、JAVA回收机制
三、四种引用
四、小结
五、安卓内存泄漏排查工具
六、内存泄漏检查与解决流程
七、常见的内存泄漏缘由
算法

一、JAVA内存解析

要想知道内存泄漏,须要先了解java中运行时内存是怎么构成的,才能知道是哪一个地方致使。话很少说,先上图 数组

java内存模型
运行时的java内存分为两大块: 线程私有(蓝色区域)、 共享数据区(黄色区域)
线程私有:主要用于存储各个线程私有的一些信息,包括:程序计数器、虚拟机栈、本地方法栈
共享数据区:主要用于存储公用的一些信息,包括:方法区(内含常量池)、堆

  1. 程序计数器:让程序中各个线程知道本身接下来须要执行哪一行。在java中多线程为抢占式(由于cpu在某一时刻只会执行一条线程),当线程切换时,须要继续哪一行便由程序计数器告知。

    举个例子:A、B两条线程,此时CPU执行从A切换至B,过了段时间从B切换回A,此时A须要从上次暂停的地方继续执行,此时从哪一行执行就是由程序计数器来提供。

    值得一提
    (1)若执行java函数时,程序计数器记录的是虚拟机字节码的地址;
    (2)若执行native方法时,程序计数器便置为了null。
    (3)在java虚拟机规范中,程序计数器是惟一没有定义OutOfMemoryError。
    bash

  2. 虚拟机栈:描述的是java方法的内存模型,平时说的“栈”其实就是虚拟机栈,其生命周期与线程相同。每一个方法(不包含native方法)执行的同时都会建立一个栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。

    值得一提:在java虚拟机规范中,此处定义了两个异常
    (1)StackOverFlowError (在递归中常看到,递归层级过深)
    (2)OutOfMemoryError
    微信

  3. 本地方法栈:是为虚拟机使用到的Native方法提供内存空间。 有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,好比主流的HotSpot虚拟机。

    值得一提:在java虚拟机规范中,此处定义了两个异常
    (1)StackOverFlowError (在递归中常看到,递归层级过深)
    (2)OutOfMemoryError
    网络

  4. 方法区:主要存储已加载是类信息(由ClassLoader加载)、常量、静态变量、编译后的代码的一些信息。 GC在这里比较少出如今这块区域。多线程

  5. 堆:存放的是几乎全部的对象实例和数组数据。 是虚拟机管理的最大的一块内存,是GC的主战场,因此也叫“GC堆”、“垃圾堆” 。

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

  6. 运行时常量池:属于“方法区”的一部分,用于存放编译器生成的各类字面量和符号引用。
    字面量:与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。
    符号引用:编译语言层面的概念,包括如下3类:
    (1) 类和接口的全限定名
    (2)字段的名称和描述符
    (3)方法的名称和描述符
    eclipse

二、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的对象有:
一、方法区中静态属性引用的对象
二、方法区中常量引用的对象
三、本地方法栈JNI中引用的对象(Native对象)
四、虚拟机栈(本地变量表)中正在运行使用的引用

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

三、四种引用

说到底,内存泄漏是由于引用的处理不正当致使的。因此,咱们接下来须要老生常谈一下java中四种引用,即:强软弱虚(引用强度依次减弱)。

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

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

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

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

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

四、小结

至此,咱们知道内存泄漏是由于堆内存中的长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经再也不须要,可是由于长生命周期对象持有它的引用而致使不能被回收。

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

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

(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来介绍,稍安勿躁。

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

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

通过前面的一段理论,可能不少小伙伴都有些不耐烦了,如今便来真正的操做。

舒适提示:理论是进阶中必要的支持,不然只是知其然而不知其因此然

(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一个质的飞跃。

七、常见的内存泄漏缘由

(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);
        setContentView(R.layout.activity_main);
        loadData();
    }
}
复制代码

这种建立Handler的方式会形成内存泄漏,因为mHandler是Handler的非静态匿名内部类的实例,因此它持有外部类Activity的引用,咱们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,因此致使该Activity的内存资源没法及时回收,引起内存泄漏,因此另一种作法为:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private void loadData() {
        //do request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<Context>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity mainActivity = (MainActivity) reference.get();
            if (mainActivity != null) {
                //do something to update UI via mainActivity
            }
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
}
复制代码

建立一个静态Handler内部类,而后对Handler持有的对象使用弱引用,这样在回收时也能够回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中仍是可能会有待处理的消息,因此咱们在Activity的Destroy时或者Stop时应该移除消息队列中的消息,

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}
复制代码

使用mHandler.removeCallbacksAndMessages(null);是移除消息队列中全部消息和全部的Runnable。固然也可使用mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。

(5)Thread

和handler同样,线程也是形成内存泄露的一个重要的源头。线程产生内存泄露的主要缘由在于线程生命周期的不可控。好比线程是 Activity 的内部类,则线程对象中保存了 Activity 的一个引用,当线程的 run 函数耗时较长没有结束时,线程对象是不会被销毁的,所以它所引用的老的 Activity 也不会被销毁,所以就出现了内存泄露的问题。

(6)系统bug

好比InputMethodManager,会持有activity而没释放,致使泄漏,须要经过反射进行打断。

若是须要更多的交流与探讨,能够经过如下微信二维码加小盆友好友。

相关文章
相关标签/搜索