Android Studio 使用Memory Monitor进行内存泄露分析

在使用Android Studio进行内存泄露分析之前,我们先回顾一下Java相关的内存管理机制,然后再讲述一下内存分析工具如何使用。

一、Java内存管理机制

1. Java内存分配策略

Java 程序运行时的内存分配策略有三种:静态分配、栈式分配和堆式分配。

对应的存储区域如下:

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
  • 栈区 :方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。
  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

2. 堆与栈的区别

栈内存:在方法体内定义的局部变量(一些基本类型的变量和对象的引用变量)都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存:用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

3. Java管理内存的机制

Java的内存管理就是对象的分配和释放问题。内存的分配是由程序员来完成,内存的释放由GC(垃圾回收机制)完成。GC 为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。这是Java程序运行较慢的原因之一。

1). 释放对象的原则:

该对象不再被引用。

2). GC的工作原理:

将对象考虑为有向图的顶点,将引用关系考虑为有向图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程为顶点开始的一棵根树。在有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象与这个根顶点不可达,那么我们认为这个对象不再被引用,可以被 GC 回收。
另外,Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象相互引用,但只要它们和根进程不可达,那么GC也是可以回收它们的。当然,除了有向图的方式,还有一些别的内存管理技术,不同的内存管理技术各有优缺点,在这里就不详细展开了。

3). Java中的内存泄漏

如果一个对象满足以下两个条件:

(1)这些对象是可达的,即在有向图中,存在通路可以与其相连

(2)这些对象是无用的,即程序以后不会再使用这些对象

就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,继续占用着内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收。

二、Android中的内存泄漏

1. 单例造成的内存泄漏 

在Android开发中,常见的单例问题造成内存泄漏的场景如下:

当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

1.如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以没有任何问题。

2.如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

当然,Application 的 context 不是万能的,所以也不能随便乱用,例如Dialog必须使用 Activity 的 Context。

2. 非静态内部类创建静态实例造成的内存泄漏

非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

3. 匿名内部类造成的内存泄漏

匿名内部类默认也会持有外部类的引用。

如果在Activity/Fragment中使用了匿名类,并被异步线程持有,如果没有任何措施这样一定会导致泄漏。

例子:Handler造成的内存泄漏。

修复方法:在 Activity 中避免使用非静态内部类或匿名内部类,比如将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。如果需要用到Activity,就通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去。另外, Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息。
代码示例如图:

4. 资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File, Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

5. 不良代码造成的内存使用压力

有些代码并不造成内存泄漏,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。比如,Adapter里没有复用convertView等。

三、使用Android Studio的Memory Monitor来分析内存泄漏情况

先看一下Android Studio 的 Memory Monitor界面:

最原始的内存泄漏排查方式如下:

重复多次操作关键的可疑的路径,从内存监控工具中观察内存曲线,看是否存在不断上升的趋势,且退出一个界面后,程序内存迟迟不降低的话,可能就发生了严重的内存泄漏。

这种方式可以发现最基本,也是最明显的内存泄漏问题,对用户价值最大,操作难度小,性价比极高。

 

下面就开始用一个简单的例子来说明一下如何排查内存泄漏。

首先,创建了一个TestActivity类,里面的测试代码如下:

@Override  
protected voidprocessBiz() {      
    mHandler = new Handler();     
    mHandler.postDelayed(newRunnable() {         
        @Override         
        public voidrun() {              
            MLog.d("------postDelayed------");         
        }      
    }, 800000L); 
}

运行项目,并执行以下操作:进入TestActivity,然后退出,再重新进入,如此操作几次后,最后最终退出TestActivity。这时发现,内存持续增高,如图所示:

这时我们可以假设,这里可能出现了内存泄漏的情况。那么,如何继续定位到内存泄漏的地址呢?这时候就得点击“Dump java heap”按钮来收集具体的信息了。

下面我们就要需要使用Android Studio生成Java Heap文件来分析内存情况了。

注意,在点击 Dump java heap 按钮之前,一定要先点击Initate GC按钮强制GC,建议点击后等待几秒后再次点击,尝试多次,让GC更加充分。然后再点击Dump Java Heap按钮。

这时候会生成一个Java heap文件并在新的窗口打开:

这时候,点击右上角的“Analyzer Task”,再点击出现的绿色按钮,让Android studio帮我们自动分析出有可能潜在的内存泄漏的地方:

如上图所示,Android studio提示有3个TestActivity对象可能出现了内存泄漏。而且左边的Reference Tree(引用树),也大概列出了该实体类被引用的路径。通过这些我们就能大概能猜到是哪里导致了内存泄漏。