天天看点

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

标签: 内存泄漏 内存溢出

内存泄漏和内存溢出的区别

内存溢出(out of memory是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。比如在我们每个Android程序在运行时系统都会给程序分配一个一定的内存空间,当程序在运行中需要的内存超出这个限制就会报内存溢出(out of memory)。

内存泄漏(memory leak是指程序在申请内存后,无法释放已申请的内存空间。多次内存无法被释放,程序占用的内存会一直增加,直到超过系统的内存限制报内存溢出。

java中为什么会发生内存泄漏

大家在学习Java的时候,可以在阅读相关书籍的时候,关于Java的优点中,第一条就是Java是通过GC来自动管理内存的回收的,程序员不需要通过调用函数来释放内存。因此,很多人认为Java不存在内存泄漏的问题,真实的情况并不是这样,尤其是我们在开发手机和平板相关的应用的时候,往往是由于内存泄漏的累计很快导致程序的崩溃。想要了解这个问题,我们需要先了解Java是如何管理内存。

Java的内存管理

Java的内存管理就是对象的分配和释放的问题,在Java中,程序员需要需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放由GC完成的。这种收支两条线的确是简化了程序员的工作。但同时,它也加重了JVM的负担。这也是Java运行较慢的原因之一。因为,GC为了正确的释放每个对象,GC必须监控每个对象的运行状态,包括对象的申请,引用,被引用,赋值GC都需要监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。

以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

Java中的内存泄漏

下面,我们就可以描述什么是内存泄漏。在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

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

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

Android内存泄漏总结

在我们开发Android程序的时候,经常会遇到内存溢出的情况,在我们这次Launcher的开发过程中,就存在内存泄漏的问题。下面结合我们在Launcher开发中遇到的实际问题,分享一下内存泄漏怎么解决。

###Android中常见内存泄漏

  1. 集合类泄漏
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性,全局性的 map 等即有静态引用),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。请看下面的示例代码,稍不注意还是很容易出现这种情况,比如我们都喜欢通过HashMap做一些缓存之类的事,这种情况就要多留一些心眼。
ArrayList list = new ArrayList();
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}
           

在上面的代码中list是一个全局变量,仅仅在后面把对象的引用置空是没有用的,因为对象被list持有,在本类生命周期没有结束的情况下,是不会被gc回收的。

  1. 单例造成的内存泄漏。
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,使单例持有的对象一直存在,很容易造成内存泄漏。比如下面一个典型的例子:
`public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
    if (instance == null) {
    instance = new AppManager(context);
    }
    return instance;
    }
    }`
           

上面的例子中如果传进来的Context是Activity,AppManager是静态的变量,它的生命周期和Application是一样的。由于该Activity一直被该该instance 一直持有,所以传进来的Activity无法被回收。将会产生内存泄漏。解决方法:此处可以传入Application,因为Application的生命周期是从开始到结束的。

  1. 非静态内部类创建静态实例造成的内存泄漏
非静态内部类默认持有该类,如果在本类中它的实例是静态的,就表示它的生命周期是和Application一样长。那么默认非静态内部类的静态实例持有了该类,该资源不会被gc掉,导致内存泄漏。
public class MainActivity extends Activity{
     
     private static LeakInstance mLeakInstance;
     @override
     public void onCreate(Bundle onsaveInstance){
        ......
     }
     
     class LeakInstance{
         .....
     }
}
           

上面的代码片段中,LeakInstance 是一个在Activity中的内部类,它有一个静态实例mLeakInstance,该静态实例的生命周期和Application是一样的,同时它默认持有了MainActivity,这样会导致Activity不会被gc掉,导致内存泄漏。

  1. 匿名内部类运行在异步线程。
匿名内部类默认持有它所在类的引用,如果把这个匿名内部类放到一个线程中取运行,而这个线程的生命周期和这个类的生命周期不一样的时候,会导致该类被线程所持有,不能释放。导致内存泄漏。请看如下示例代码:
public class MainActiviy extends Activity{
   
   private Therad mThread = null;
   
   private Runnable myRunnable = new Runnable{
        public void run{
           ......
        
        }
   }
       protected void onCreate(Bundle onSaveInstance){
               .......
              mThread = new Thread(myRunnable);
              mThread.start();
       }
}
           

在上面的例子中myRunnable 持有了MainActiviy,mThread的生命周期和Activity不一样,MainActiviy会被持有直到Thread运行结束。导致内存泄漏。

  1. Handler 造成的内存泄漏

Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。可以看看下面的列子:

public MainActivity extends Activity{

    private Handler mHandler = new Handler();
    
    protected void onCreate(Bundle onSaveInstance){
        .......
        mHandler.postDelay(new Runnable(){
          
        
        },1000*1000);
    }
}
           

上述代码中mHandler把delay很久,实际持有了MainActivity,如果在此Activity死掉,那么他是无法被回收的。需要等待mHandlder释放持有的资源。

如何发现内存泄漏

MAT

1.直接通过观察Android Monitor的memory直观的观察,例如我们在开发Launcher的时候,Launcher的Activity在横竖屏切换的时候就出现了内存泄漏的情况,这时候Memory的值会不断的变大,且通过手动点击GC,无法释放内存。

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

2.通过MAT工具查找

Java 内存泄漏的分析工具有很多,但众所周知的要数 MAT(Memory Analysis Tools) 和 YourKit 了。由于篇幅问题,我这里就只对 MAT 的使用做一下介绍。–> MAT 的安装

MAT分析heap的总内存占用大小来初步判断是否存在泄露

打开 DDMS 工具,在左边 Devices 视图页面选中“Update Heap”图标,然后在右边切换到 Heap 视图,点击 Heap 视图中的“Cause GC”按钮,到此为止需检测的进程就可以被监视。

Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:

进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。

所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。

MAT分析hprof来定位内存泄露的原因所在

这是出现内存泄露后使用MAT进行问题定位的有效手段。

A)Dump出内存泄露当时的内存镜像hprof,分析怀疑泄露的类:

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

B)分析持有此类对象引用的外部对象

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

C)分析这些持有引用的对象的GC路径

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

D)逐个分析每个对象的GC路径是否正常

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

从这个路径可以看出是一个antiRadiationUtil工具类对象持有了MainActivity的引用导致MainActivity无法释放。此时就要进入代码分析此时antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context导致节目退出后MainActivity无法销毁,那一般都属于内存泄露了)。

MAT对比操作前后的hprof来定位内存泄露的根因所在

为查找内存泄漏,通常需要两个 Dump结果作对比,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去

A) 第一个HPROF 文件(usingFile > Open Heap Dump ).

B)打开Histogram view.

C)在NavigationHistory view里 (如果看不到就从Window >show view>MAT- Navigation History ), 右击histogram然后选择Add to Compare Basket .

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

D)打开第二个HPROF 文件然后重做步骤2和3.

E)切换到Compare Basket view, 然后点击Compare the Results (视图右上角的红色”!”图标)。

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

F)分析对比结果

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

可以看出两个hprof的数据对象对比结果。

通过这种方式可以快速定位到操作前后所持有的对象增量,从而进一步定位出当前操作导致内存泄露的具体原因是泄露了什么数据对象。

注意:

如果是用 MAT Eclipse 插件获取的 Dump文件,不需要经过转换则可在MAT中打开,Adt会自动进行转换。

而手机SDk Dump 出的文件要经过转换才能被 MAT识别,Android SDK提供了这个工具 hprof-conv (位于 sdk/tools下)

首先,要通过控制台进入到你的 android sdk tools 目录下执行以下命令:

./hprof-conv xxx-a.hprof xxx-b.hprof

例如 hprof-conv input.hprof out.hprof

此时才能将out.hprof放在eclipse的MAT中打开。

下面将给大家介绍一个屌炸天的工具 – LeakCanary 。

#####LeakCanary

什么是LeakCanary 呢?为什么选择它来检测 Android 的内存泄漏呢?

别急,让我来慢慢告诉大家!

LeakCanary 是国外一位大神 Pierre-Yves Ricau 开发的一个用于检测内存泄露的开源类库。一般情况下,在对战内存泄露中,我们都会经过以下几个关键步骤:

1、了解 OutOfMemoryError 情况。

2、重现问题。

3、在发生内存泄露的时候,把内存 Dump 出来。

4、在发生内存泄露的时候,把内存 Dump 出来。

5、计算这个对象到 GC roots 的最短强引用路径。

6、确定引用路径中的哪个引用是不该有的,然后修复问题。

很复杂对吧?

如果有一个类库能在发生 OOM 之前把这些事情全部都搞定,然后你只要修复这些问题就好了。LeakCanary 做的就是这件事情。你可以在 debug 包中轻松检测内存泄露。

一起来看这个例子(摘自 LeakCanary 中文使用说明,下面会附上所有的参考文档链接):

class Cat{

}
class Box{
  Cat hiddenCat;
}

class Docker {
   //静态变量,生命周期和Classload一样。
   static Box cainter;
}
        // 薛定谔之猫
Cat schrodingerCat = new Cat();
box.hiddenCat = schrodingerCat;
Docker.container = box;
           
创建一个RefWatcher,监控对象引用情况。
// 我们期待薛定谔之猫很快就会消失(或者不消失),我们监控一下
refWatcher.watch(schrodingerCat);
           

当发现有内存泄露的时候,你会看到一个很漂亮的 leak trace 报告:

GC ROOT static Docker.container

references Box.hiddenCat

leaks Cat instance

我们知道,你很忙,每天都有一大堆需求。所以我们把这个事情弄得很简单,你只需要添加一行代码就行了。然后 LeakCanary 就会自动侦测 activity 的内存泄露了。

public class ExampleApplication extends Application {
  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}
           

然后你会在通知栏看到这样很漂亮的一个界面:

一文教你搞定Android内存泄漏,内存溢出如何发现内存泄漏

以很直白的方式将内存泄露展现在我们的面前。

Demo

一个非常简单的 LeakCanary demo: 一个非常简单的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo

接入

在 build.gradle 中加入引用,不同的编译使用不同的引用:

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

如何使用

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

RefWatcher refWatcher = {...};

// 监控
refWatcher.watch(schrodingerCat);
           

LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。

在Application中进行配置 :

public class ExampleApplication extends Application {

  public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context.getApplicationContext();
    return application.refWatcher;
  }

  private RefWatcher refWatcher;

  @Override public void onCreate() {
    super.onCreate();
    refWatcher = LeakCanary.install(this);
  }
}
           

使用 RefWatcher 监控 Fragment:

public abstract class BaseFragment extends Fragment {

  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}
           

使用 RefWatcher 监控 Activity:

public class MainActivity extends AppCompatActivity {

......
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
        //在自己的应用初始Activity中加入如下两行代码
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);
    refWatcher.watch(this);

    textView = (TextView) findViewById(R.id.tv);
    textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            startAsyncTask();
        }
    });

}

private void async() {

    startAsyncTask();
}

private void startAsyncTask() {
    // This async task is an anonymous class and therefore has a hidden reference to the outer
    // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
    // the activity instance will leak.
    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            // Do some slow work in background
            SystemClock.sleep(20000);
            return null;
        }
    }.execute();
}
           

}

工作机制

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

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

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

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

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

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

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

ok,这里就不再深入了,想要了解更多就到 作者 github 主页。

参考文档:

IBM:Java内存泄漏

LeakCanery:LeakCanery中文使用手册

MAT:MAT使用教程