常见内存泄漏场景以及解决办法

2,360 阅读11分钟

实例化

对象是类的一个实例,创建对象的过程也叫类的实例化。对象是以类为模板来创建的。比如Car car = new Car();,我们就创造了一个Car的实例(Create new class instance of Car)

引用

某些对象的实例化需要其它的对象实例,比如ImageView的实例化就需要Context对象,就是表示ImageView对于Context持有引用(ImageView holds a reference to Context)。

有向图

在每条边上都标有有向线段的图称为有向图,Java中的garbage collection采用有向图的方式进行内存管理,箭头的方向表示引用关系,比如 B ← A ,就是B中需要A,B引用A。

可达

有向图D={S,R}中,对于Si,Sj 属于S,如果从Si到Sj有任何一条通路存在,则可称Si可达Sj。也就是说,当B ← A中间箭头断了,就称作不可达,这时A就不可达B了。

Shallow heap

表示当前对象所消耗的内存

Retained heap

表示当前对象所消耗的内存加上它引用的内存总合

垃圾回收

Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对虚拟机中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证虚拟机中的内存空间,防止出现内存泄露和溢出问题。

什么情况需要垃圾回收

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常GC采用有向图的方式记录并管理堆中的所有对象,通过这种方式确定哪些对象时“可达”,哪些对象时“不可达”。当对象不可达的时候,即对象不再被引用的时候,就会被垃圾回收。

网上有很多文档介绍可达的关系了,如图,在第六行的时候,o2改变了指向,Obj2就不再引用main的了,即他它们是不可达的,Obj2就可能在下次的GC中被回收。

什么是内存泄露

当你不再需要某个实例后,但是这个对象却仍然被引用,防止被垃圾回收(Prevent from being bargage collected)。这个情况就叫做内存泄露(Memory Leak)。

在Android中,内存泄漏很多情况下都是因为生命周期长短不一造成的,下面列举几个常见的原因:

Handler导致内存泄漏

public class MainActivity extends AppCompatActivity {
  private final Handler mHandler2 = new Handler() {
    @Override public void handleMessage(Message msg) {
      super.handleMessage(msg);
    }
  };
}

以上问题实际上是非静态内部类持有外部类导致内存溢出的问题,关于这个问题我们下面还会讨论。

IDE提示如下图所示:

原理大致如下: 当 Android 应用程序启动时,framework 会为该应用程序的主线程创建一个 Looper 对象。Looper 对象包含一个简单的消息队列 Message Queue,并且能够循环的处理队列中的消息。这些消息包括大多数应用程序 framework 事件,例如 Activity 生命周期方法调用、button 点击等,这些消息都会被添加到消息队列中并被逐个处理。主线程的 Looper 对象会伴随该应用程序的整个生命周期。

当我们在主线程中实例化一个 Handler 对象后,会自动与主线程 Looper 的消息队列关联起来。所有发送到消息队列的消息 Message 都会拥有一个对 Handler 的引用,而此时当前 Activity 如果已经结束/销毁,而 Handler 由于是非静态内部类就会持有外部类的对象,抓住当前 Activity 对象不放,此时就极有可能导致内存泄漏。

下面的情况同样会导致 Handler 内存泄漏

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

这个程序很简单,我们可以脑补一下,它应该是启动了又瞬间关闭,但是事实真的是关闭了吗?

稍有常识的人可以看出,它发送了一个Message,将在十分钟后运行,也就是说Message将被保持引用达到10分钟,这就照成了至少10分钟的内存泄露。

最后正确的代码如下:

public class MainActivity extends AppCompatActivity {

  private final MyHandler mHandler=new MyHandler(this);
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mHandler.postDelayed(sRunable,10000);
    finish();
  }

  /**
   * 匿名类/非静态类内部class会保持对它所在Activity的引用,
   * 使用时要注意它们的生命周期不能超过Activity,
   * 否则要用static inner class
   */
  private static final Runnable sRunable=new Runnable() {
    @Override public void run() {

    }
  };
  private static class MyHandler extends Handler{
    //GC是按照有向图是否可达来判断对象实例是否有用
    //弱引用,当MainActivity被销毁时,GC会立刻回收这个对象,避免内存泄漏
    //如果不在需要某个实例,却仍然被引用,这个情况叫做内存泄露
    private final WeakReference<MainActivity> mActivity;

    private MyHandler(MainActivity activity) {
      mActivity =new WeakReference<MainActivity>(activity);
    }

    @Override public void handleMessage(Message msg) {
      MainActivity activity=mActivity.get();
      if(activity!=null){
        super.handleMessage(msg);
      }
    }
  }
}

WeakRefrence 的相关概念:弱引用对象的存在不会阻止它所指向的对象变被垃圾回收器回收。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后垃圾收集器会把这个弱可达对象标记为可终结(finalizable)的,这样它们随后就会被回收。与此同时或稍后,垃圾收集器会把那些刚清除的弱引用放入创建弱引用对象时所登记到的引用队列(Reference Queue)中。

  • 强引用(StrongReference) 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

  • 软引用(SoftReference) 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

  • 弱引用(WeakReference) 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  • 虚引用(PhantomReference) “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

静态变量导致内存泄漏

public class SecondActivity extends AppCompatActivity{
  private static Context sContext;


  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_second);

    sContext = this;
    findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        finish();
      }
    });
  }

}

如上也有可能导致内存泄漏,导致内存泄漏的原因是:静态变量持有当前 Activity。导致当前 Activity 结束时候,静态变量仍然持有它的引用。深层次探究就要清楚静态变量在 Android 中的生命周期了。可以参见[Android静态变量的生命周期].这篇文章讲述的非常清楚,大力推荐。

单例模式导致内存泄漏

public class AppManager {

  private static AppManager sAppManager;
  private Context context;


  private AppManager(Context context) {
    this.context=context;
  }


  public static AppManager getInstance(Context context) {
    if(sAppManager==null){
      sAppManager=new AppManager(context);
    }
    return sAppManager;
  }
}

内存溢出检测如下:

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

1.如果是 Application 的 Context:OK,这样是可以的,因为单例的生命周期和 Application 的一样长 。

2.如果是 Activity 的 Context:当这个 Context 所对应的 Activity 退出时,它的内存并不会被回收,因为单例对象持有该 Activity 的引用。

解决办法如下所示:

public class AppManager {

  private static AppManager sAppManager;
  private Context context;


  private AppManager(Context context) {
    /**
     * 为防止内存泄漏
     * 这样不管传入什么 Context 最终将使用 Application 的 Context,
     * 而单例的生命周期和应用的一样长,这样就防止了内存泄漏
     */
    this.context=context.getApplicationContext();
  }


  public static AppManager getInstance(Context context) {
    if(sAppManager==null){
      sAppManager=new AppManager(context);
    }
    return sAppManager;
  }
}

非静态内部类持有外部类的实例

public class ThirdActivity extends AppCompatActivity {

  private static MySample sMySample=null;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_third);

    if(sMySample==null){
      sMySample=new MySample();
    }
  }
  class MySample{

  }
}

上述代码利用LeakCanary检测结果如下:

由于我们在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据(避免了资源的重复创建),这种写法却会造成内存泄漏,同样因为非静态内部类持有外部类对象的原因。正确的做法为: 将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用 ApplicationContext。

线程造成的内存泄漏

Runnable 是一个匿名内部类( AsyncTask 存在匿名内部类的情况),对当前 Activity 都有一个隐式引用。如果在当前 Activity 在销毁之前,任务还未完成,那么将导致 Activity 的内存资源无法回收,导致内存泄漏。实例代码如下:

public class FourthActivity extends AppCompatActivity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_fourth);
    sample();
    finish();
  }

  private void sample() {
    new MyThread().start();
  }

  private class MyThread extends Thread {
    @Override public void run() {
      while (true) {
        SystemClock.sleep(1000);
      }
    }
  }
}

不出意料,我们很快就收到了内存泄漏的通知。正确做法用静态内部类即可,如下所示:

  private static class MyThread extends Thread {
    @Override public void run() {
      while (true) {
        SystemClock.sleep(1000);
      }
    }
  }

属性动画导致内存泄漏

属性动画中有一类无线循环的动画,如果在当前 Activity 中播放此类动画,并且没有在结束的时候(onDestory)去停止该动画,那么动画会一直播放下去,尽管在界面上无法看见动画的运转,但是在此时 Activity 的 View 会被动画所持有,而 View 又持有当前 Activity,最终导致 Activity 无法被释放。代码如下:

public class FifthActivity extends AppCompatActivity {

  private View view;
  private ObjectAnimator mAnimator;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_fifth);
    view = findViewById(R.id.view);

    mAnimator = ObjectAnimator.ofFloat(view,"translationX",0,400);
    mAnimator.setDuration(1000);
    mAnimator.setRepeatCount(ValueAnimator.INFINITE);
    mAnimator.start();

  }
}

解决办法很简单,在 OnDestory() 中去取消动画即可。

  @Override protected void onDestroy() {
    super.onDestroy();
    if(mAnimator!=null){
      mAnimator.cancel();
    }
  }

Dialog 导致的内存泄漏

在当前 Dialog 所依附的 Activity 销毁之前,我们没有去将当前的 Dialgo 销毁(dismiss) 话也是很容易导致内存泄漏的。

使用 LeakCanary 去检测内存泄漏

在 build.gradle 中:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }

在 Application 中: public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

最后不要忘了去注册 Application 哦!

当你调试 App 时候,一旦发现内存吃紧。LeakCanary 会自动以通知的方式提醒你。Easy to go.当然啦,LeakCanary 的用法还有很多,更详细的就请大家移步 LeakCanary 主页查看啦。

参考文章

Android内存泄露分析

常见内存泄漏场景以及解决办法

"常见的八种导致 APP 内存泄漏的问题"

Android 内存泄漏案例和解析

《Android 开发艺术探索》