Android 内存泄露总结(附内存检测工具)

Java 中的内存分配

主要是分三块:git

  • 静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量。github

  • 栈区:当方法执行时,会在栈区内存中建立方法体内部的局部变量,方法结束后自动释放内存。算法

  • 堆区:一般存放 new 出来的对象。由 Java 垃圾回收器回收。segmentfault

栈与堆的区别

栈内存用来存放局部变量和函数参数等。它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高。当超过变量的做用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间能够被从新使用。缓存

堆内存用于存放对象实例。在堆中分配的内存,将由Java垃圾回收器来自动管理。在堆内存中频繁的 new/delete 会形成大量内存碎片,使程序效率下降。并发

对于非静态变量的储存位置,咱们能够粗暴的认为:app

  • 局部变量位于栈中(其中引用变量指向的对象实体存在于堆中)。ide

  • 成员变量位于堆中。由于它们属于类,该类最终被new成对象,并做为一个总体储存在堆中。函数

四种引用类型的介绍

GC 释放对象的根本原则是该对象再也不被引用(强引用)。那么什么是强引用呢?工具

强引用(Strong Reference)

咱们日常用的最多的就是强引用,以下:

IPhotos iPhotos = new IPhotos();

JVM 宁肯抛出 OOM ,也不会让 GC 回收具备强引用的对象。强引用不使用时,能够经过 obj = null 来显式的设置该对象的全部引用为 null,这样就能够回收该对象了。至于何时回收,取决于 GC 的算法,这里不作深究。

软引用(Soft Reference)

SoftReference<String> softReference = new SoftReference<>(str);

若是一个对象只具备软引用,那么在内存空间足够时,垃圾回收器就不会回收它;若是内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就能够被使用。

软引用曾常常被用来做图片缓存,然而谷歌如今推荐用 LruCache 替代,由于 LRU 更高效。

In the past, a popular memory cache implementation was a SoftReference
or WeakReference bitmap cache, however this is not recommended.
Starting from Android 2.3 (API Level 9) the garbage collector is more
aggressive with collecting soft/weak references which makes them
fairly ineffective. In addition, prior to Android 3.0 (API Level 11),
the backing data of a bitmap was stored in native memory which is not
released in a predictable manner, potentially causing an application
to briefly exceed its memory limits and crash. 原文

大体意思是:由于在 Android 2.3 之后,GC 会很频繁,致使释放软引用的频率也很高,这样会下降它的使用效率。而且 3.0 之前 Bitmap 是存放在 Native Memory 中,它的释放是不受 GC 控制的,因此使用软引用缓存 Bitmap 可能会形成 OOM。

弱引用(Weak Reference)

WeakReference<String> weakReference = new WeakReference<>(str);

与软引用的区别在于:只具备弱引用的对象拥有更短暂的生命周期。由于在 GC 时,一旦发现了只具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。不过,因为垃圾回收器是一个优先级很低的线程,所以不必定会很快发现那些只具备弱引用的对象- -。

虚引用(PhantomReference)

顾名思义,就是形同虚设,与其余几种引用都不一样,虚引用并不会决定对象的生命周期,也没法经过虚引用得到对象实例。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,若是发现它还有虚引用,就会在回收对象的内存以前,把这个虚引用加入到与之关联的引用队列中。程序能够经过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。

Android的垃圾回收机制简介

Android 系统里面有一个 Generational Heap Memory 模型,系统会根据内存中不一样的内存数据类型分别执行不一样的 GC 操做。

该模型分为三个区:

  • Young Generation

    1. eden

    2. Survivor Space

      1. S0

      2. S1

  • Old Generation

  • Permanent Generation

Young Generation

大多数 new 出来的对象都放到 eden 区,当 eden 区填满时,执行 Minor GC(轻量级GC),而后存活下来的对象被转移到 Survivor 区(有 S0,S1 两个)。 Minor GC 也会检查 Survivor 区的对象,并把它们转移到另外一个 Survivor 区,这样就总会有一个 Survivor 区是空的。

Old Generation

存放长期存活下来的对象(通过屡次 Minor GC 后仍然存活下来的对象) Old Generation 区满了之后,执行 Major GC(大型 GC)。

在Android 2.2 以前,执行 GC 时,应用的线程会被暂停,2.3 开始添加了并发垃圾回收机制。

Permanent Generation

存放方法区。通常存放:

  • 要加载的类的信息

  • 静态变量

  • final常量

  • 属性、方法信息

60 FPS

这里简单的介绍一下帧率的概念,以便于理解为何大量的 GC 容易引发卡顿。

App 开发时,通常追求界面的帧率达到60 FPS(60 帧/秒),那这个 FPS 是什么概念呢?

  • 10-12 FPS 时能够感觉到动画的效果;

  • 24 FPS,能够感觉到平滑连贯的动画效果,电影经常使用帧率(不追求 60 FPS 是节省成本);

  • 60 FPS,达到最流畅的效果,对于更高的FPS,大脑已经难以察觉区别。

Android 每隔 16 ms发出 VSYNC 信号,触发对 UI 的渲染(即每 16 ms绘制一帧),若是整个过程保持在 16 ms之内,那么就会达到 60 FPS 的流畅画面。超过了 16 ms就会形成卡顿。那么若是在 UI 渲染时发生了大量 GC,或者 GC 耗时太长,那么就可能致使绘制过程超过 16 ms从而形成卡顿(FPS 降低、掉帧等),而咱们大脑对于掉帧的状况十分敏锐,所以若是没有作好内存管理,将会给用户带来很是很差的体验。

再介绍一下内存抖动的概念,本文后面可能会用到这个概念。

内存抖动

短期内大量 new 对象,达到 Young Generation 的阈值后触发GC,致使刚 new 出来的对象又被回收。此现象会影响帧率,形成卡顿。

内存抖动在 Android 提供的 Memory Monitor 中大概表现为这样:

图片描述

Android中常见的内存泄露及解决方案

集合类泄露

若是某个集合是全局性的变量(好比 static 修饰),集合内直接存放一些占用大量内存的对象(而不是经过弱引用存放),那么随着集合 size 的增大,会致使内存占用不断上升,而在 Activity 等销毁时,集合中的这些对象没法被回收,致使内存泄露。好比咱们喜欢经过静态 HashMap 作一些缓存之类的事,这种状况要当心,集合内对象建议采用弱引用的方式存取,并考虑在不须要的时候手动释放。

单例形成的内存泄露

单例的静态特性致使其生命周期同应用同样长。

有时建立单例时若是咱们须要Context对象,若是传入的是Application的Context那么不会有问题。若是传入的是Activity的Context对象,那么当Activity生命周期结束时,该Activity的引用依然被单例持有,因此不会被回收,而单例的生命周期又是跟应用同样长,因此这就形成了内存泄露。

解决办法一:在建立单例的构造中不直接用传进来的context,而是经过这个context获取Application的Context。代码以下:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context.getApplicationContext();// 使用Application 的context
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

第二种解决方案:在构造单例时不须要传入 context,直接在咱们的 Application 中写一个静态方法,方法内经过 getApplicationContext 返回 context,而后在单例中直接调用这个静态方法获取 context。

非静态内部类形成的内存泄露

在 Java 中,非静态内部类(包括匿名内部类,好比 Handler, Runnable匿名内部类最容易致使内存泄露)会持有外部类对象的强引用(如 Activity),而静态的内部类则不会引用外部类对象。

非静态内部类或匿名类由于持有外部类的引用,因此能够访问外部类的资源属性成员变量等;静态内部类不行。

由于普通内部类或匿名类依赖外部类,因此必须先建立外部类,再建立普通内部类或匿名类;而静态内部类随时均可以在其余外部类中建立。

Handler内存泄露能够关注个人另外一篇专门针对Handler内存泄露的文章:连接

WebView 的泄漏

Android 中的 WebView 存在很大的兼容性问题,有些 WebView 甚至存在内存泄露的问题。因此一般根治这个问题的办法是为 WebView 开启另一个进程,经过 AIDL 与主进程进行通讯, WebView 所在的进程能够根据业务的须要选择合适的时机进行销毁,从而达到内存的完整释放。

AlertDialog 形成的内存泄露

new AlertDialog.Builder(this)
        .setPositiveButton("Baguette", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                MainActivity.this.makeBread();
            }
        }).show();

DialogInterface.OnClickListener 的匿名实现类持有了 MainActivity 的引用;

而在 AlertDialog 的实现中,OnClickListener 类将被包装在一个 Message 对象中(具体能够看 AlertController 类的 setButton 方法),并且这个 Message 会在其内部被复制一份(AlertController 类的 mButtonHandler 中能够看到),两份 Message 中只有一个被 recycle,另外一个(OnClickListener 的成员变量引用的 Message 对象)将会泄露!

解决办法:

  • Android 5.0 以上不存在此问题;

  • Message 对象的泄漏没法避免,可是若是仅仅是一个空的 Message 对象,将被放入对象池做为后用,是没有问题的;

  • 让 DialogInterface.OnClickListener 对象不持有外部类的强引用,如用 static 类实现;

  • 在 Activity 退出前 dismiss dialog

Drawable 引发的内存泄露

Android 在 4.0 之后已经解决了这个问题。这里能够跳过。

当咱们屏幕旋转时,默认会销毁掉当前的 Activity,而后建立一个新的 Activity 并保持以前的状态。在这个过程当中,Android 系统会从新加载程序的UI视图和资源。假设咱们有一个程序用到了一个很大的 Bitmap 图像,咱们不想每次屏幕旋转时都从新加载这个 Bitmap 对象,最简单的办法就是将这个 Bitmap 对象使用 static 修饰。

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);
    TextView label = new TextView(this);
    label.setText("Leaks are bad");

    if (sBackground == null) {
        sBackground = getDrawable(R.drawable.large_bitmap);
    }
    label.setBackgroundDrawable(sBackground);

    setContentView(label);
}

可是上面的方法在屏幕旋转时有可能引发内存泄露,由于,当一个 Drawable 绑定到了 View 上,实际上这个 View 对象就会成为这个 Drawable 的一个 callback 成员变量,上面的例子中静态的 sBackground 持有 TextView 对象的引用,而 TextView 持有 Activity 的引用。当屏幕旋转时,Activity 没法被销毁,这样就产生了内存泄露问题。

该问题主要产生在 4.0 之前,由于在 2.3.7 及如下版本 Drawable 的 setCallback 方法的实现是直接赋值,而从 4.0.1 开始,setCallback 采用了弱引用处理这个问题,避免了内存泄露问题。

资源未关闭形成的内存泄露

  • BroadcastReceiver,ContentObserver 之类的没有解除注册

  • Cursor,Stream 之类的没有 close

  • 无限循环的动画在 Activity 退出前没有中止

  • 一些其余的该 release 的没有 release,该 recycle 的没有 recycle… 等等。

总结

咱们不难发现,大多数问题都是 static 形成的!

  • 在使用 static 时必定要当心,关注该 static 变量持有的引用状况。在必要状况下使用弱引用的方式来持有一些引用

  • 在使用非静态内部类时也要注意,毕竟它们持有外部类的引用。(使用 RxJava 的同窗在 subscribe 时也要注意 unSubscribe)

  • 注意在生命周期结束时释放资源

  • 使用属性动画时,不用的时候请中止(尤为是循环播放的动画),否则会产生内存泄露(Activity 没法释放)(View 动画不会)

几种内存检测工具的介绍

  • Memory Monitor

  • Allocation Tracker

  • Heap Viewer

  • LeakCanary

Memory Monitor

位于 Android Monitor 中,该工具能够:

  • 方便的显示内存使用和 GC 状况

  • 快速定位卡顿是否和 GC 有关

  • 快速定位 Crash 是否和内存占用太高有关

  • 快速定位潜在的内存泄露问题(内存占用一直在增加)

  • 可是不能准确的定位问题

Allocation Tracker

该工具用途:

  • 能够定位代码中分配的对象类型、大小、时间、线程、堆栈等信息

  • 能够定位内存抖动问题

  • 配合 Heap Viewer 定位内存泄露问题(能够找出来泄露的对象是在哪建立的等等)

使用方法:在 Memory Monitor 中有个 Start Allocation Tracking 按钮便可开始跟踪 在点击中止跟踪后会显示统计结果。

Heap Viewer

该工具用于:

  • 显示内存快照信息

  • 每次 GC 后收集一次信息

  • 查找内存泄露的利器

使用方法: 在 Memory Monitor 中有个 Dump Java Heap 按钮,点击便可,在统计报告左上角选按 package 分类。配合 Memory Monitor 的 initiate GC(执行 GC)按钮,可检测内存泄露等状况。

LeakCanary

重要的事情说三遍:

for (int i = 0; i < 3; i++) {
            Log.e(TAG, "检测内存泄露的神器!");
        }

LeakCanary 具体使用再也不赘述,自行 Google。

相关文章
相关标签/搜索