Android Memory Leak

Android内存泄漏总结


基础知识


1)泄露的对象

首先我们来了解程序运行时,所需内存的分配策略:

按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、堆区和栈区。他们的功能不同,对他们使用方式也就不同。

静态存储区(方法区):内存在程序编译的时候就已经分配好,这块内存在程序整个运行期间都存在。它主要存放静态数据、全局static数据和常量。

栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存(JAVA则依赖垃圾回收器)。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉。

接下来我们集中说下堆和栈的区别:

在函数中(说明是局部变量)定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,JAVA就在栈中为这个变量分配内存空间,当超过变量的作用域后,JAVA会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。

堆内存用于存放所有由new创建的对象(内容包括该对象其中的所有成员变量)和数组。在堆中分配的内存,由JAVA虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G),所以堆的空间比较灵活,比较大。栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设置)。

对于堆,频繁的new/delete会造成大量内存碎片,使程序效率降低。对于栈,它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。
——因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)
——因为它们属于类,类对象终究是要被new出来使用的。
我们这里说的内存泄露,是针对,也只针对堆内存,他们存放的就是引用指向的对象实体。


2)泄露的原因

为了判断JAVA中是否有内存泄露,我们首先必须了解JAVA是如何管理(堆)内存的。JAVA的内存管理就是对象的分配和释放问题。在JAVA中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器Garbage Collection,GC完成的,程序员不需要通过调用函数来释放内存,但它只能回收无用并且不再被其它对象引用的那些对象所占用的空间。

Java的内存垃圾回收机制是从程序的主要运行对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链,组成无法回收的对象集合,而其他孤立对象(集)就作为垃圾回收。GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

在JAVA中,这些无用的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。虽然,我们有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据JAVA语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC。通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。

至此,我们来看看JAVA中需要被回收的垃圾:

{
Person p1 = new Person();
……
}

引用句柄p1的作用域是从定义到}处,执行完这对大括号中的所有代码后,产生的Person对象就会变成垃圾,因为引用这个对象的句柄p1已超过其作用域,p1失效,在栈中被销毁,因此堆上的Person对象不再被任何句柄引用了。 因此person变为垃圾,会被回收。

从上面的例子和解释,可以看到一个很关键的词:引用。

通俗的讲,通过A能调用并访问到B,那就说明A持有B的引用,或A就是B的引用,B的引用计数+1。

  1. 比如Person p1 = new Person();通过p1能操作Person对象,因此p1是Person的引用;

  2. 比如类O中有一个成员变量是I类对象,因此我们可以使用o.i的方式来访问I类对象的成员,因此o持有一个i对象的引用。

GC过程与对象的引用类型是严重相关的,我们来看看JAVA对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference。

强弱软虚引用表

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。讲多一步,这里的软引用/弱引用一般是做什么的呢?

软/弱引用可以和一个引用队列ReferenceQueue联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:

public class CacheBySoftRef {
    
    // 首先定义一个HashMap,保存软引用对象。

    private Map>imageCache = new HashMap>();

    // 再来定义一个方法,保存Bitmap的软引用到HashMap。

    public void addBitmapToCache(String path) {

        // 强引用的Bitmap对象

        Bitmap bitmap = BitmapFactory.decodeFile(path);

        // 软引用的Bitmap对象

        SoftReferencesoftBitmap = new SoftReference(bitmap);

        // 添加该对象到Map中使其缓存

        imageCache.put(path, softBitmap);

    }

    // 获取的时候,可以通过SoftReference的get()方法得到Bitmap对象。

    public Bitmap getBitmapByPath(String path) {

        // 从缓存中取软引用的Bitmap对象

        SoftReference softBitmap = imageCache.get(path);

        // 判断是否存在软引用

        if (softBitmap == null) {

            return null;

        }

        // 通过软引用取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空 ,如果未被回收,则可重复使用,提高速度。

        Bitmap bitmap = softBitmap.get();

        return bitmap;

    }

}

使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

回到我们的问题,为什么内存会泄露?

堆内存中的长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因。


常见原因及实践


常见原因

  1. 集合类

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

  2. 单例模式

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

  3. ANDROID组件或特殊集合对象的使用

    BraodcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。

    不要直接对 Activity 进行直接引用作为成员变量,如果不得不这么做,请用 private WeakReference mActivity 来做,相同的,对于Service 等其他有自己声明周期的对象来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。

  4. HANDLER

    要知道,只要 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。如上所述,Handler 的使用要尤为小心,否则将很容易导致内存泄露的发生。

  5. THREAD内存泄露

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

  6. 一些不良代码造成的内存压力

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

    6.1 Bitmap 没调用 recycle()。

    Bitmap对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null。因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recycle() 就是针对 C 部分的内存释放。

    6.2 构造 Adapter 时,没有使用缓存的 convertView。

优秀实践
  1. 对activity等组件的引用应该控制在activity的生命周期之内;如果不能就考虑使用getApplicationContext或者getApplication,以避免activity被外部长生命周期的对象引用而泄露。

  2. 在代码复审的时候关注长生命周期对象:全局性的集合、单例模式的使用、类的static变量等等。

  3. 尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

  4. Handler的持有的引用对象最好使用弱引用,资源释放时也可以清空Handler里面的消息。比如在Activity onStop或者onDestroy的时候,取消掉该Handler对象的Message和Runnable,removeCallbacks(Runnable r)或removeMessages(int what)或removeCallbacksAndMessages(null)等。

  5. 线程Runnable执行耗时操作,注意在页面返回时及时取消或者把Runnable写成静态类: a) 如果线程类是内部类,改为静态内部类。 b) 线程内如果需要引用外部类对象如context,需要使用弱引用。

  6. 在Java的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋空,如清空对图片等资源有直接引用或者间接引用的数组(使用array.clear();array = null),最好遵循谁创建谁释放的原则。


排查方法以及工具


MAT内存分析

  1. 需要的软件

    Android Studio;
    Memory Analyzer,下载地址:http://www.eclipse.org/mat/downloads.php,下载完成后进行解压,双击MemoryAnalyzer运行。

  2. 如何使用

    2.1 打开Android Studio,Run/Debug要进行分析的工程。

    2.2 启动Android Studio工具栏上的Android Device Monitor。

    2.3 在Android Device Monitor中选中我们要进行分析的应用,然后点击update heap。

    2.4 在应用内进行操作后,点击DUMP HPROF file来保存hprof文件。(建议保存到sdk\platform-tools目录下,后面转换比较方便)

    2.5 进入到sdk\platform-tools目录下,启动命令行,输入如下:./hprof-conv inputName.hprof outputName.hprof。(由于都在一个目录下,输入输出文件可以不用指定位置)

    2.6 启动Memory Analyzer,选择File>open heap dump,打开刚才转换好的文件。

    2.7 点击Leak Suspects会显示几个内存泄露疑点,在其中怀疑的地方,点击 Details 就可以看到具体的内存使用情况。

    2.8 点击Histogram,在出现的菜单中选中一项右键选择 list objects>with incoming references 将列出该类的实例,如果想查看某个实例没被释放的原因,可以右健 Path to GC Roots>exclude all phantom/weak/soft etc. reference ,这样可以去掉所有的虚/弱/软引用,以便我们快速查看某个对象的 GC Root。(表头可以进行排序,在表的第一行可以输入正则表达式来匹配结果)

    2.9 有时候为查找内存泄漏,我们通常需要两个Dump结果作对比,这时我们可以打开Windows>Navigator History面板,将两个表的Histogram结果都添加到Compare Basket中去 ,然后点击面板右上角的红色叹号,得到对比结果,对比条件也是可设置的。

  3. 一个例子

    一个典型的分析内存泄漏的过程:

    3.1 使用Heap查看当前堆大小为23.00M;

    3.2 添加一个页后堆大小变为23.40M;

    3.3 将添加的一个页删除,堆大小为23.40M;

    3.4 多次操作,结果仍相似,说明添加/删除页存在内存泄漏 (也应注意排除其它因素的影响);

    3.5 Dump出操作前后的hprof文件 1.hprof,2.hprof,用mat打开,并得到histgram结果;

    3.6 使用HomePage字段过滤histgram结果,并列出该类的对象实例列表,看到两个表中的对象集合大小不同,操作后比操作前多出一个HomePage,说明确实存在泄漏;

    3.7 将两个列表进行对比,找出多出的一个对象,用查找GC Root的方法找出是谁串起了这条引用线路,定位结束。

LeakCanary内存分析
  1. 开始使用

    在 build.gradle 中加入引用:

    dependencies {
        debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.4-beta2’
        releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2’
        testCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2’
    }
    在Application中:

    public class VideoApplication extends Application {
        @Override
        public void onCreate() {
                super.onCreate();
                LeakCanary.install(this);
        }
    }
    在 debug build 中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。

  2. 如何使用

    使用 RefWatcher 监控那些本该被回收的对象。

    RefWatcher refWatcher = {...};
    // 监控
    refWatcher.watch(schrodingerCat);
    LeakCanary.install() 
    //会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
    public class VideoApplication extends Application {
    
    public static RefWatcher getRefWatcher(Context context) {
        VideoApplication application = (VideoApplication) context.getApplicationContext();
        return application.refWatcher;
    }
    
    private RefWatcher refWatcher;
    
    @Override
    public void onCreate() {
            super.onCreate();
            refWatcher = LeakCanary.install(this);
    }
    }

    使用 RefWatcher 监控 Fragment:

    @Override
    public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = VideoApplication.getRefWatcher(getActivity());
        refWatcher.watch(this);
    }
  3. 工作机制

    3.1 watch() 创建一个KeyedWeakReference 到要被监控的对象。

    3.2 然后在后台线程检查引用是否被清除,如果没有,调用GC。

    3.3 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个.hprof 文件中。

    3.4 在另外一个进程中的HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。

    3.5 得益于唯一的 reference key, HeapAnalyzer 找到KeyedWeakReference,定位内存泄露。

    3.6 HeapAnalyzer 计算到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。

    3.7 引用链传递到 APP 进程中的DisplayLeakService, 并以通知的形式展示出来。

  4. 如何复制LEAK TRACE

    在 Logcat 中,你可以看到类似这样的 leak trace:

    In com.example.leakcanary:1.0:1 com.example.leakcanary.MainActivity has leaked:
    \* GC ROOT thread java.lang.Thread. (named 'AsyncTask #1')
    \* references com.example.leakcanary.MainActivity$3.this$0 (anonymous class extends android.os.AsyncTask)
    \* leaks com.example.leakcanary.MainActivity instance
    \* Reference Key: e71f3bf5-d786-4145-8539-584afaecad1d
    \* Device: Genymotion generic Google Nexus 6 - 5.1.0 - API 22 - 1440x2560 vbox86p
    \* Android Version: 5.1 API: 22
    \* Durations: watch=5086ms, gc=110ms, heap dump=435ms, analysis=2086ms
  5. LEAK TRACE之外

    有时,leak trace 不够,你需要通过 MAT 或者 YourKit 深挖 dump 文件。

    通过以下方法,你能找到问题所在:

    5.1 查找所有的squareup.leakcanary.KeyedWeakReference 实例。

    5.2 检查key 字段。

    5.3 Find the KeyedWeakReference that has a key field equal to the reference key reported by LeakCanary.

    5.4 找到 key 和 和 logcat 输出的 key 值一样的KeyedWeakReference。

    5.5 referent 字段对应的就是泄露的对象。

    5.6 剩下的,就是动手修复了。最好是检查到 GC root 的最短强引用路径开始。


参考资料


内存泄露从入门到精通三部曲之基础知识篇

内存泄露从入门到精通三部曲之常见原因与用户实践

内存泄露从入门到精通三部曲之排查方法篇

LeakCanary 中文使用说明

Gaoge

A student.

Beijing, China ggao.me