Android学习笔记19-内存泄漏分析

今天来简单的介绍下怎么分析android应用的内存泄漏问题。

首先我们要明白为什么会有内存泄漏,主要有2种情况:
a.全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用的怪物。
b.活在Activity生命周期之外的线程。没有清空对Activity的强引用。

1、了解下4种级别对象的引用:

从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
    强引用(Strong reference):
        常见形式如:A a = new A();等
    软引用(Soft Reference):
        A a = new A();
        SoftReference<A> srA = new SoftReference<A>(a);
        软引用所指示的对象进行垃圾回收需要满足如下两个条件:
        1.当其指示的对象没有任何强引用对象指向它;
        2.当虚拟机内存不足时。
    弱引用(Weak Reference):
        A a = new A();
        WeakReference<A> wrA = new WeakReference<A>(a);
        WeakReference不改变原有强引用对象的垃圾回收时机,一旦其指示对象没有任何强引用对象时,此对象即进入正常的垃圾回收流程。
    虚引用(Phantom Reference):

2、了解下内存的知识:

JAVA是在JVM所虚拟出的内存环境中运行的,JVM的内存可分为三个区:堆(heap)、栈(stack)和方法区(method)。
    栈(stack):栈最显著的特征是:LIFO(Last In, First Out, 后进先出),栈中只存放基本类型和对象的引用(不是对象)。
    堆(heap):堆内存用于存放由new创建的对象和数组。
        在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。
    方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

查看设备内存的方法:
    a.查看meminfo文件
        cat /proc/meminfo
        cat /proc/meminfo |grep Mem
    b.更详细的信息可以使用dumpsys命令
        dumpsys meminfo  查看总的内存使用
        dumpsys meminfo com.example.testleakmemory  查看单个应用的内存使用情况
        dumpsys meminfo 27606  通过pid查看单个应用的内存使用情况

3、工具准备:

工欲善其事,必先利器。我们要使用的是DDMS+MAT(Memory Analysis Tool)来分析。
DDMS是ADT自带的调试工具。
MAT的下载网址:http://www.eclipse.org/mat/downloads.php

这里写图片描述
我自己在线安装一直都不成功,各种错误。后面直接在下载(Stand-alone Eclipse RCP Applications)版本。
开始的时候下载了64位的版本,发现运行exe文件的时候老是报 “failed to load the jni shared jvm.dll” 这个错误。
后面又下载32位了发现可以运行。查看本地的java版本,发现是32位的(我电脑是64位的),这就可以解释通。
D:\android-sdk-windows-4.3\platform-tools>java -version
java version “1.7.0_51”
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) Client VM (build 24.51-b03, mixed mode, sharing)

4、构造一个有内存泄漏的代码:

    MainActivity.java:
        public class MainActivity extends Activity {
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
            }
            public void eventClick(View v) {//点击跳转到第二个页面
                Intent intent = new Intent(this, Second.class);
                startActivity(intent);
            }
        }

    Second.java:
        public class Second extends Activity {
            private List<String> list = new ArrayList<String>();//全局的list
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_second);
                // 模拟Activity一些其他的对象
                for (int i = 0; i < 200000; i++) {
                    list.add(new String("hello world!"));
                }
                new MyThread().start();// 开启线程
            }

            public class MyThread extends Thread {
                @Override
                public void run() {
                    super.run();
                    try {// 模拟耗时操作,线程开启10分钟
                        Thread.sleep(1000*60*10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }   

5、获取hprof文件:

mat分析需要导出hprof文件。可以先导出泄漏之前的hprof文件,在操作app,导出泄漏之后的hprof。

这里写图片描述

操作之后的内存图:
这里写图片描述

dump hprof文件
这里写图片描述

6、转化hprof文件:

cmd到当前的目录下,把你需要转化的hprof文件也可以拷贝到当前的目录
hprof-conv 1.hprof before.hprof

这里写图片描述

7、使用mat打开文件开始分析-直方图:

这个时候我们获取了操作之前的before.hprof文件 和 操作之后的 after.hprof文件。
打开mat工具,导入(Open Heap Dump ...)hprof开始分析。

主界面
这里写图片描述

我们分析最常用到的就是Histogram(直方图)和 Dorminator Tree(支配树)
a.打开Histogram(直方图)他列举了每个对象的统计。它可以列出任意一个类的实例数。
    比如你需要查看MainActivity,可以使用正则表达式 .*Main.*  (注意大小写)
    可以列出与MainActivity相关的类

这里写图片描述

b.选中com.example.testleakmemory.Second,右击,选择“Merge Shortest Paths to GC Roots”,
    再选择选择“exclude all phantom/weak/soft etc.references”
    (排查虚引用/弱引用/软引用等)因为被虚引用/弱引用/软引用的对象可以直接被GC给回收.
    在JAVA中是通过可达性(Reachability Analysis)来判断对象是否存活,这个算法的基本思想是通过一系列的称谓"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走得路径称为引用链

这里写图片描述

c.如果存在GC Roots链,即存在内存泄露问题。
    除了使用Merge Shortest Paths to GC Roots 我们还可以使用
    List object - With outgoing References   显示选中对象持有那些对象
    List object - With incoming References  显示选中对象被那些外部对象所持有
    Show object by class - With outgoing References  显示选中对象持有哪些对象, 这些对象按类合并在一起排序
    Show object by class - With incoming References  显示选中对象被哪些外部对象持有, 这些对象按类合并在一起排序

这里写图片描述

8、使用mat打开文件开始分析-Dorminator Tree支配树):

 a.通过 Dorminator Tree(支配树),可以直观地反映一个对象的retained heap,根据retained heap进行排序.
    shallow heap:指的是某一个对象所占内存大小。
    retained heap:指的是一个对象的retained set所包含对象所占内存的总大小。
    它主要可以用于诊断一个对象所占内存为什么会不断膨胀,一个对象膨胀,就说明它对应到支配树中的子树就越来越庞大。

这里写图片描述

 b.通过dominator_tree查看单一对象所持有的对象
    右击对象--Java Basics -- Open In Dominator Tree
    很实用 

 注意:MAT工具上显示的size的大小单位是: Bytes 。例如 21414072 = 20.4M

这里写图片描述

 在MAT中可以查看到有类似如下的显示
    com.example.main.MainActivity$1
    com.example.main.MainActivity$2
    这个 $1 表示第一个匿名类的大小。
    $2、$3这样排下去。

9、使用的小技巧1:

a.操作前后两个hprof的对比:
    dump出前后2个hprof文件
    使用mat工具打开,在Navigation History这个视图中,右击histogram--Add to Compare Basket
    Window -- Compare Basket,里面会有2个histogram。点击对比就可以了。
    上面这个对比结果不利于查找差异,还可以调整对比选项
    Difference from base Table

这里写图片描述

10、使用的小技巧2:

我们使用对比发现了操作后的内存中多了很多 char[] (我们构造的20万个String未释放)。
在after.hprof的直方图视图中
    a.可以右击 -- Immediate dominators(查看引用者) -- 可以发现Second这个activity持有20万个,基本可以判断Second这个页面有内存泄露。
        我们再查看那个变量持有了这么多。

这里写图片描述

    b.右击这个Second -- 选择 Dominated Objects(注意一定是这个不要选择Objects里面的) -- Merge Shortest Paths to GC Roots -- 
        exclude all phantom/weak/soft etc.references

这里写图片描述

        c.一步步看谁持有了这个Second不释放

这里写图片描述

    备注:把这个问题解决了之后,再分析发现还有个输入法问题。
        android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper 
            .
            .
            list
        可以确定是输入法持有了activity的引用,导致Second activity里面list这个变量未正常释放。
    (输入法是单例的,只会持有一个Second的,所以不管你打开多少次这个页面,泄露的大小是固定的。网上查阅这个是系统的一个bug,
        如果页面占用内存不是太多,主要是全局变量,那么泄露的就可以接受)

11、实际中遇到的内存泄漏问题:

在实际的应用中遇到了以下一些的内存泄露问题:
a.将一些View定义为static,例如窗口显示:我在其它的类里面直接调用静态的方法来设置静态的View上面的文本。
    因为static的生命周期很长,导致这个View持有activity的引用不释放,导致activity销毁了,资源还得不到释放(如果有图片资源泄露的更多)。
    解决的方法:
    将View不要定义为static,Handler类定义为静态的,使用弱引用来持有activity的引用。

b.在工具类Information类中,定义了一个静态的WiFiManager变量wm
    在使用的时候,直接将context传到这个工具类中,wm使用到了这个context获取服务,activity销毁了,wm不会销毁,导致activity资源不释放,内存泄露。
    解决的方法:
    在工具类中不用传递进来的context,直接获取全局的Context
    或者context.getApplicationContext();来获取

c.将context传进了测试类中,类里面又将context定义为static,导致activity销毁内存不释放。(和上面的情况类似)
    解决方式:
    测试完成,将静态的context设置为null,断掉GC Root链路

d.使用第三方的jar包,显示gif图像导致的内存泄露:
    我们启动了gif,但是jar提供的停止的方法中不能停止它里面的子线程。
    使用mat分析到时jar包里面的子线程引用了activity,这样子线程不结束,activity就释放不了
    解决方法:
    我们没有jar的源码,通过查看class显示的私有属性有个isRunning,但是没有提供方法设置。我通过反射去设置了这个变量的属性。问题解决,可以正常释放了。
    if(null != gv_toolbox_distribute_gif){
        try {
            Class<? extends GifView> clazz = gv_toolbox_distribute_gif.getClass();
            Field f = clazz.getDeclaredField("isRun");
            f.setAccessible(true);
            f.set(gv_toolbox_distribute_gif, false); 
        } catch (Exception e) {
            LogUtils.d(TAG, "失败:" + e.getMessage());
        }
    }

e.自定义一个WaveCircle动画水波纹,里面使用了postDelay方法。导致内存泄露(网上有比较多的这个分析案例)
    通过mat工具分析发现,有个ThreadLocal老是持有activity,继续往下看,在getRunQueue里面有2个Runnable未执行。
    后面解决方法:
    1.在调用stop动画的时候睡500ms,测试有效。
    2.我使用发消息的方法stop动画,测试也有效(暂未研究源码,也许是消息执行需要等待一段时间)

f.有个等待的状态Dialog没有正常关闭
    通过mat分析到时有个WindowManger持有了activity的引用不释放。后面查代码发现dialog根本就没有关闭。
    解决方法:
    在dialog显示完成结束的时候,需要手动dismiss掉这个dialog,实际测试OK

g.使用了帧动画,没有正常结束
    通过mat分析到有很大的Bitmap未释放,把原图导出来看下,发现是帧动画的原图,检查代码发现,原来开启了动画,当时没有结束动画。
    解决方法:
    在动画显示完成的时候stop掉就OK了。

**总结:
以上只是分析了一个简单的例子,基本的流程就是这样,以后遇到了复杂的问题,按照这个思路来分析也能逐步的找到问题的所在。
平时我们写代码的时候要多注意这些性能的问题。能避免的尽量避免,不然遇到了泄漏的问题后面来分析花费的代价会更大。
好的习惯很重要!!!**

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值