如何使用MAT分析Android应用内存泄露

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用MAT分析Android应用内存泄露相关的知识,希望对你有一定的参考价值。

参考技术A 工具:MAT,Eclispe。 在Eclipse中新建一个android工程,名字叫做:memoryleak。功能很简单,只有一个MainActivity和一个ImageUtil。我们在MainActivity中使用ImageUtil来加载图片。注意我们的ImageUtil是单例类。在创建的时候需要传入context。 2.应用起来以后,转屏。转屏以后,MainActivity会重建,导致ImageUtil重新加载图片。很不幸的是,因为ImageUtil并没有重建,还持有之前的Context,也就是之前的那个MainActivity实例。多次转屏以后,我们在Eclipse的DDMS里面,导出内存映像文件。导出的文件叫做:com.example.memoryleak.hprof。 3.DDMS导出的内存映像文件并不能被MAT直接使用。需要转换一下。在命令行输入:hprof-conv com.example.memoryleak.hprof memoryleak.hprof。 4.用MAT打开memoryleak.hprof。再点击“Histogram”, 5.在打开的页面中,点击可以看到详细的引用信息,就可以看没有出错了。

Android常见内存泄露分析及MAT与Profiler内存分析时间

目录

1.Java内存分配策略 

2.堆与栈的区别

3.Java管理内存的机制

4.Java中的内存泄露

5.Android中常见的内存泄露

6.Android中内存泄露的排查与分析

7.总结


1.Java内存分配策略 

Java程序运行时的内存分配策略有三种:静态分配、栈式分配和堆式分配。对应的存储区域如下:

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

2.堆与栈的区别

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

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

例子:

public class A{
        int a=0;
        B b = new B();
        public void test(){
            int a1 = 1;
            B b1 = new B();
        }
}
A object = new A();
  • A类内的局部变量都存在栈中,包括基本的数据类型a1和引用变量b1,b1指向的B对象实体存于在堆中;
  • 引用变量object存在于栈中,而object指向的对象实体存在于堆中,包括这个对象的所有成员变量a和b,而引用变量b指向的B类型对象实体存在于堆中;

3.Java管理内存的机制

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

释放对象的原则:该对象不再被引用。

GC的工作用原理:

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

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

另外:Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象相互引用,但是只要他们和跟进程不可达,那么GC也是可以回收他们的。当然,除了有向图的方式,还有一些别的内存管理技术,不同内存管理技术各有优缺点,这里就不详细展开了。

4.Java中的内存泄露

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

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

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

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

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

5.Android中常见的内存泄露

(1)单例造成的内存泄漏

public class MainActivity extends Actvity implements View.OnClickListener{
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bitmap bitmap = ImageUtils.getInstance(this).getBitmap(R.drawable.arrow);
    }
}

public class ImageUtils{
    private static ImageUtils imageUtils;
    private Context context;
    private ImageUtils(Context context){
        this.context = context;
    }
    public static ImageUtils getInstance(Context context){
        if(imageUtils == null){
            imageUtils = new ImageUtils(context);
        }
        return imageUtils;
    }    
}

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

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

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

当然,Application的Context不是万能的,所以不能随便乱用,例如Dialog必须使用Activity的Context。对于这部分有兴趣的读者可以自行搜索相关资料。

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

public class MainActivity extends AppCompatActivity{
   private static TestResource mResource = null;
   @Override
   protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        if(mManager == null){
            mManger = new TestResource();
        }
        //...
   } 
   class TestResource{
       //... 
   }
}

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

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

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private static final String TAG = "MainActivity";
    private Runnable ref1 = new MyRunnable();
    private Runnable ref2 = new MyRunnable(){
        public void run(){}
    };
    public static class MyRunnable extends Thread{
        public void run(){}
    }
    @Override
   protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
   }
}

ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这个两个引用的内存:

可以看到,ref1没有什么特别的。但是ref2这个匿名类的实现对象多了一个引用:

this$0这个引用执行MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Activity生命周期不一致的时候,就会造成Activity的泄漏。

例子:Handler造成的内存泄漏

public class MainActivity extends Activity implements View.OnClickListener{
    private static final String TAG = "MainActivity";
    private final Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg){    
            //...
        }
    }

    @Override 
    protected void onCreate(Bundle savedIntanceState){
        super.onCreate(savedInstanceState);
        setContextView(R.layout.activity_main);
        mHandler.postDelayed(new Runnable(){
            public void run(){}
        }, 1000*60*10L);
    }
}

在该MainActivity中声明一个延迟10分钟执行的消息Message,mHandler将其push进了消息队列MessageQueue里。当该Activity被finish()掉时,延迟执行任务的Message还会继续存在于主线程中,它持有该Activity的Handler引用,然后又因为Handler为匿名内部类,它会持有外部类的引用(在这里就是MainActivity),此时finish()掉Activity就不会被回收了,从而造成内存泄漏。

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

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private StaticHandler staticHandler = new StaticHandler(this);

    private static class StaticHandler extends Handler{

        private WeakReference<MainActivityNew> weakReference;

        public StaticHandler(MainActivityNew activity){
            weakReference = new WeakReference<MainActivityNew>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
//            super.handleMessage(msg);
            MainActivityNew activity = weakReference.get();
            if(activity != null){

            }
        }
    }

    private static class StaticRunnable extends Thread{
        @Override
        public void run() {
//            super.run();
        }
    }



    @Override
    public void onClick(View v) {

    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        staticHandler.postDelayed(new StaticRunnable(), 1000*60*10L);
    }

    @Override
    protected void onDestroy() {
        staticHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}

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

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

(5)一些不良代码造成的内存压力

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

6.Android中内存泄露的排查与分析

(1)利用Android Studio的Memory Monitor来检测内存情况

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

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

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

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

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

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

protected void processBiz(){
    mHandler = new Handler();
    mHandler.postDelayed(new Runnable(){
        public void run(){
            Log.d("MainActivity","----postDelayed-----");
        }
    },800000L);
}

运行项目,并执行以下操作:进入TestActivity,然后退出,再重新进入,如此操作几次后,最后退出TestActivity。这是发现,内存持续增高,如图所示(不断打开关闭TestActivity查看Total总内存大小会缓慢的增高):

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

(2)使用Android Studio生成Java Heap文件来分析内存情况

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

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

 旧版可能需要点击“Analyzer Task”,再点击出现的绿色按钮,让Android Studio帮我们自动分析可能出现的潜在内存泄漏的地方,新版本不需要自动帮我们分析了内存泄漏(4.1.2):

如上图所示,Android Studio提示有四个TestActivity对象可能存在内存泄漏。而且下面(Reference Tree)引用树,也大概列出了该实体类的引用路径。如果是一些比较简单的内存泄漏情况,仅仅看这里就可以大概能猜到是哪里导致了内存泄漏。

(3)使用Memory Analyzer(MAT)来分析内存泄漏

MAT是Eclipse出品的一个插件,当然也有独立的版本。下载链接:MAT下载地址

 在这里先提醒一下:MAT并不会准确的告诉你我们哪里可能发生内存泄漏,而是会提供一大堆的数据和线索,我们需要根据自己的实际的代码和业务逻辑去分析这些数据,判断到底是不是真的发生了内存泄漏。

MAT支持对标准格式hprof文件进行内存分析,所以,我们要现在Android Studio里先把Java heap文件转换成标准格式的hprof文件,具体步骤如下:

选择对应Heap Dump文件,右键点击选择Export导出的hprof文件:

 导出的hprof文件需要转换成标准的hprof格式文件:

hprof-conv.exe memory-20210808T201824.hprof memory.hprof(标准格式的)

运行cmd打开命令行,cd到\\ android-sdk-windows\\tools所在目录,并输入命令hprof-conv xxxxx.hprof yyyyy.hprof,其中xxxxx.hprof为原始文件,yyyyy.hprof为转换过后的文件。转换过后的文件自动放在android-sdk-windows\\tools 目录下。

导出标准的hprof文件后,在MAT工具里导入,则看到以下界面:

 MAT提供了非常多的功能,这里我们只学习几个常用的就可以了。上图那个饼状图展示了最大的几个对象所占内存的比例,这张图中提供的内容并不多,我们可以忽略它。在这个饼状图的下方就有几个非常有用的工具了。

Histogram:直方图,可以列出内存中每个对象的名字、数量以及大小。

Dominator Tree:会将所有内存中的对象按照大小进行排序,并且我们可以分析对象之间的引用结构;

(1)Dominator Tree

 从上图可以看到右边存在3个参数。Retained Heap表示这个对象以及它所持有的其他引用(包括直接和间接)所占的总内存,因此从上图中看,前两行的Retained Heap是最大的,分析内存泄漏时,内存最大的对象也是最应该怀疑的。

另外大家应该可以注意到,在每一行的最左边都有一个文件类型的图标,这些图标有的左下角带有一个红色的点,有的则没有。带有红色的对象就表示可以被GC Roots访问到,可以被GC Roots访问到的对象都是无法被回收的。那么这就可以说明所有带红色的对象都是泄露的对象吗?当然不是,因为有些对象系统需要一直使用,本来就不应该被回收。

如果发现有的对象右边有写着System Class,那么说明这是一个由系统管理的对象,并不是由我们自己创建并导致内存泄漏的对象。

根据我们在Android studio的Java heap文件的提示,TestActivity对象有可能发生了内存泄漏,于是我们直接在上面搜TestActivity(这个搜索功能也是很强大的):

左边的inspector可以查看对象内部的各种信息:

 当然,如果你觉得按照默认的排序方式来查看不方便,你还可以自行设置排序的方式:

  • Group by class
  • Group by class loader
  • Group by package

从上图可以看出,我们搜到了4个TestActivity对象,一般退出某个activity后,就结束一个activity的生命周期,应该被GC正常回收才对的。通常情况下,一个activity应该只有1个实例对象,但是现在居然有四个TestActivity对象,说明之前的操作,产生了4个TestActivity对象,并且无法被系统回收掉。

接下来继续查看引用路径:

对于TestActivity对象右键 -> Merge Shortest Paths to GC Roots(当然,这里也可以选择Path To GC Roots) -> exclude all phantom/weak/soft etc. references;

为什么选择exclude all phantom/weak/soft etc. references呢?因为弱引用等是不会阻止对象被垃圾回收器回收的,所以我们这里直接把它排除掉:

 接下来就能看到引用路径关系图了:

 从上图可以看出,TestActivity是被this$0所引用的,它实际上是匿名类对当前类的引用。this$0又被callback所引用,接着它又被Message中一串的next多引用...,到这里,我们就已经分析出内存泄漏的原因了,接下来就是去改善存在问题的代码了。

(2)Histogram

这里是当前应用程序中所有的对象的名字、数量和大小全部都列出来了,那么Shallow Heap有什么意思呢?就是当前对象自己所占内存的大小,不包含引用关系的。

上图当中,byte[]对象Shallow Heap最高,说明我们应用程序中用了很多byte[]类型的数量,比如说图片。可以通过右键->List objects->with incoming references来查看具体是谁在使用这些byte[].

当然,除了一般的对象,我们还可以专门查看线程对象的信息:

 Histogram中可以显示对象的数量,比如说我们怀疑TestActivity中有可能存在内存泄漏,就可以在主线程上右键->List objects ->with incoming references查看TestActivity实例,如下所示:

 接下来对着TestActivity右键->List objects ->with incoming references查看具体的TestActivity实例

注释:

List objects ->with incoming references:表示该对象入接点(被该对象引用的对象)

List objects ->with outgoing references: 表示该对象出接点(引用该对象的对象)

如果想要查看内容内存泄漏的具体原因,可以通过任意一个TestActivity的实例右键->Merge Shortest Paths to GC Roots(当然,这是也可以选择Path To GC Roots) ->exclude all phantom/weak/soft etc. references,如下图所示:

这可以看出,Histogram和Dominator Tree两种方式下操作都是差不多的,只是两种统计图展示的侧重点不太一样,实际操作中,根据需求选择不同的方式即可。

 (3)两个hprof文件对比

为了排查内存泄漏,经常需要做一些前后的对比。下面简单说一下两种对比方式:

a.直接对比

工具栏最右边有个"Compare to another heap dump"的按钮,只要点击,就可以生成对比后的结果。(注意,要现在MAT中打开要对别的hprof文件,才能选择对比的文件):

 b.添加到compare basket里对比

在window菜单下选择compare basket:

在文件的Histogram view模式下,在navigation history下选择add to compare basket:

然后就可以通过Compare Tables来进行对比了:

 

7.总结

最后,还是要再次提醒一下,工具是死的,人是活的,MAT也没有办法保证一定可以将内存泄漏的原因找出来,还是需要门对程序的代码有足够多的了解,知道哪些对象是存活的,以及它们存货的原因,然后再结合MAT给出的数据来进行具体的分析,这样才有可能把一些隐藏得很深的问题原因给找出来。

参考:

使用Android Studio和MAT进行内存泄漏分析

Android 内存优化之一:MAT 使用入门

Android内存优化之二:MAT使用进阶

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

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

以上是关于如何使用MAT分析Android应用内存泄露的主要内容,如果未能解决你的问题,请参考以下文章

Android常见内存泄露分析及MAT与Profiler内存分析时间

使用MAT分析Java内存

Android常见内存泄露

Android基础性能检测与分析

Linux Kernel模块内存泄露分析

Analyzer tool(MAT)分析JVM内存泄露案例