应用内存泄露起因与解决方案分析

java gc机制

java内存管理与c/c++不同,java使用garbage collection机制,由虚拟机管理内存。在大部分虚拟机(包括android的ART)中,都采用了“可达性”分析算法来进行内存管理。

原理是:选取某几个root节点,从root开始层层遍历,如果找不到对该对象的引用链,则该对象被标记为不可达,等待gc回收。

内存泄漏的起因

如果引用链中长期存在着对该对象的引用(强引用),则该对象一直不能被gc销毁。一两次并没有多大影响,如果频繁发生,则可用内存会逐渐不足,在某次申请内存时会发生OOM,导致程序崩溃。

这里强调是强引用,下面简单介绍下java中的几种引用类型:

  • SoftReference : 实现某些对内存敏感的缓存,内存足够时就会保留不被回收,内存不够时被gc销毁。
  • WeakReference:实现一些规范化的映射关系,当key或value没有被引用时可以自动回收,该引用类型不会对该对象在gc中的回收产生任何影响,因此如果想持有一个对象的引用,但是又不想干涉它的生命周期,使用WeakReference
  • PlantomReference:实现了预验清理的工作,同样不会干涉对象的生命周期,但是它的get方法总是返回null,不能获得引用的对象,其保存了ReferenceQueue的轨迹,允许获知对象何时从内存中清除。

所以如果需要使用在可能发生内存泄漏的环境中引用ActivityBitmap等,使用WeakReference最好。

泄露常见例子

由上文可知,泄露是由于长期持有对象的强引用导致,所以我们可以用以下方法强行制造泄露。

static

static修饰的变量或者引用,它们的所属的是类,生命周期与类的生命周期一致,贯穿App的启动到关闭。所以如果用static修饰一个大对象的引用,就会产生泄露。例如:

static Activity activity;

static View view;

Activity是四大组件之一,其对象占有的内存较大,用static修饰容易发生泄露。
View虽然占有的内存并不大,但是其构造函数中需要传入Context引用,一般我们传入的是Activity,也就间接持有了Activity的引用,容易泄露。

innerClass

非静态内部类会持有外部类的引用。具体见这篇文章。大致上就是说在编译阶段,编译器会给非静态加上一个成员变量,其类型与外部类类型相同,在构造方法的参数上添加上外部类的一个引用变量,并在初始化内部类的时候传入外部类的实例。如下:

//以下并不符合java代码规范,仅演示在编译阶段添加的代码

public class Test(){

    public static void mian(String[] args){
        new InnerTest(this); // 将当前实例引用传给内部类构造函数的第二个参数
    }

    class InnerTest{
        Test test; // 添加一个引用变量
        InnerTest(InnerTest this, Test test){ // 添加一个当前类的this变量和外部类的引用变量
            this.test = test;  // 对引用变量赋值
        }
    }
}

一旦非静态内部类实例长期存活,由于强引用的关系,外部类实例也长期存活,这样就造成了可能的内存泄漏。

例如AsyncTask处理后台任务,我们很多时候采用匿名内部类去创建:

void leakAsyncTask(){
        new AsyncTask<Void, Void, Void>(){
            @Override
            protected Void doInBackground(Void... voids) {
                while (true){
                    ....
                }
                return null;
            }
        }
    }

在AS中写出上述代码,AS会提醒我们需要使用静态内部类否则会出现内存泄漏,无独有偶,当我们使用ThreadHandler的时候同样会提醒我们去创建静态内部类。

register

当我们使用第三方库的时候,比如ButterKnife等等,有时候需要获取系统服务,我们在创建对象的时候会将Activity实例的引用传过去进行register或者bind,而它们在运行的时候我们不易察觉,如果我们没有在Activity被销毁之前取消订阅或者绑定,由这些后台程序持有着我们Activity的引用,就会造成内存泄漏。

泄漏检测

准备工作

AS中的Android Profiler是专门用来检测应用的运行情况:

这里写图片描述

左上角的垃圾箱形状的按钮是用来强制进行一次gc,第二个按钮是Dump java heap,这个按钮可以产生一个当前java堆的.hprof文件,用来记录当前时刻java堆中的内存情况。

而图中各种颜色的含义都在图上标明了,可以看到蓝色的变化很大,并且过段时间就会骤降,很显然这代表了java的堆内存,可以很清晰地看到各个时刻的内存情况。

肉眼观察

下面看看真实的内存泄露是什么样子,我在一个Activity中启动了另外一个Activity,在后者中增加一个static变量mContext,并将该Activity实例引用赋值给该变量,显然按照上面的说法是会出现泄露的,我们看看结果:

这里写图片描述

图中的上升沿是我点击启动另一个Activity时的内存变化,由于static变量一直持有着Activity的引用,所以gc时无法销毁,可以看到与上面图的区别就是只有上升沿没有下降沿。长时间肯定会出现内存崩溃。

自动分析

在上面频繁启动Activity后点击Dump java heap按钮,过一会我们可以看到AS为我们Dump出此时的内存记录。

这里写图片描述

找到所在包刚刚定义的类,如下:

这里写图片描述

可以明显看到本来应该大小相似的两个类,由于持有了static类型的变量,其占有的内存大小是第一个Activity的好几倍,如果频繁这样点击,最终肯定会造成内存泄漏:

第二个Activity中拥有6个实例变量

还可以导出Dump的文件然后用MAT进行分析,但是最简便的方法当然是:

使用Square公司的Leakcanary进行分析,此处是地址

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值