内存优化——内存泄漏

什么是内存泄漏?

程序中已动态分配的的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费。本质上是长生命周期的对象持有短生命周期对象的强引用,从而导致短生命周期对象无法被回收,则出现了内存泄漏的现象。
内存泄漏会带来以下几个问题:

  • 应用可用内存减少;
  • 频繁的gc,应用性能下降;
  • 严重时会发生内存溢出,即OOM Error;

为了了解内存泄漏,我们先来了解一下 Java 的引用类型。

Java中的几种引用类型

从jdk1.2版本开始把对象引用分成四种级别,从而使程序能更加灵活的控制对象生命周期,四种级别由高到底分别是强引用软引用弱引用虚引用,这里只介绍四种引用和gc的关系。

  • 强引用:无法被gc回收,当内存不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,也不会回收强引用对象。
  • 软引用:只有当内存不足时才会被回收,如果内存足够,gc扫描到软引用也不会回收它。
  • 弱引用:当gc线程扫描内存区域的时候,只要发现有弱引用就会回收它的内存,不管内存够不够用。
  • 虚引用:随时会被回收,不能通过虚引用来取得一个对象实例,一般用来跟踪对象被gc的时机。

了解了几种引用之后,可以发现除了强引用会导致内存泄漏之外,其实软引用同样也会,因为在内存够用的时候,软引用也是无法被gc回收的。而弱引用和虚引用则遇到gc就被回收,所以弱引用也是常用来避免内存泄漏的方法之一。

发生内存泄漏的场景

1、非静态内部类/匿名内部类
举个栗子:

public class MainActivity extends AppCompatActivity {

    // 匿名内部类 持有外部类MainActivity的引用
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
        }
    };

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

        // 1、匿名内部类 mHandler持有外部类 MainActivity的引用
        Message message = Message.obtain();
        mHandler.sendMessageDelayed(message, 50000);

        // 2、这里的 Handler不是匿名内部类, 但 Runnable是一个匿名内部类
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 50000);

        // 3、这里的 MyHandler是一个内部类,持有外部类引用
        MyHandler myHandler = new MyHandler();
        myHandler.sendMessageDelayed(message, 50000);
    }

    class MyHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }

}

Handler是很容易发生内存泄漏的工具,这里举了3个内存泄漏的例子,原因就是非静态内部类持有外部类对象的强引用,这时MainActivity退出后想要回收内存,但是Handler的任务还在等待依然持有MainActivity的强引用,内存无法回收,于是发生了内存泄漏。

解决方式:

  • 在Activity的onDesdory方法中移除任务。
    mHandler.removeCallbacksAndMessages(null);
  • 使用静态内部类,静态内部类不持有外部类引用,如果真的需要使用外部类对象,要用弱引用WeakReference包装,注意弱引用在使用时记得判空,因为它被gc扫到会直接回收内存。
   static class MyHandler extends Handler{
       WeakReference<MainActivity> mActivity;

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

       @Override
       public void handleMessage(Message msg) {
           super.handleMessage(msg);
           MainActivity mainActivity = mActivity.get();
           // 弱引用在使用时记得判空
           if (mainActivity != null){
               System.out.println(mainActivity.toString());
           }
       }
   }

Tip:这里要注意,并不是所有的非静态内部类和匿名内部类都会发生内存泄漏,只是他们持有了外部类的引用,如果他们的对象被其他生命周期更长的对象持有,外部类的对象就间接被持有不能及时得到回收,才会导致内存泄漏。Handler中发送延迟消息时,Handler对象和Runnable对象都会被消息队列持有,他们又持有Activity对象,所以Activity退出时无法及时回收。

2、单例或者静态成员

public class MyManager {

    private volatile static MyManager sManager;
    private Context mContext;

    private MyManager(Context context) {
        mContext = context;
    }

    public static MyManager getInstance(Context context){
        if (sManager == null){
            synchronized (MyManager.class){
                if (sManager == null){
                    sManager = new MyManager(context);
                }
            }
        }
        return sManager;
    }
}

这是一个标准的单例类,构造方法要传入一个Context类型的参数,如果我们传入的是Activity对象,很可能在这个Activity已经关闭了,MyManager还持有它的强引用,因为静态变量的生命周期是和应用生命周期一致的,自然这里就发生了内存泄漏。

解决方案

  • 获取Application对象。
    private MyManager(Context context) {
        mContext = context.getApplicationContext();
    }
  • 弱引用保护。
    private WeakReference<Context> mContext;

    private MyManager(Context context) {
        mContext = new WeakReference<>(context);
    }

3、集合类

    List<Object> list = new ArrayList<>();
    
    private void addObj(){
        Object obj = new Object();
        list.add(obj);
    }

如果集合添加了一些对象,后来对象需要销毁回收,此时集合中依然持有他们的强引用,比如观察者模式或者EventBus,当集合中的对象要销毁时需要及时remove掉,避免内存泄漏。

4、其他情况
除了以上几种常见情况,还有:

  • 资源未关闭,如FileOutputStream未close,数据库游标Cursor未close等;
  • 注册的资源未反注册,如广播、EventBus,RxBus等等
  • 静态成员使用完及时释放置空。
  • webview的内存泄漏,可以把webview单独放在一个进程中,不占用主进程内存。
  • ......

内存泄漏的检测

1、Profiler + MAT
模拟一个简单的内存泄漏例子:

public class SecondActivity extends AppCompatActivity {

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

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                
            }
        },10000);
    }
}

就是上面举过的匿名内部类的内存泄漏例子,把它放在SecondActivity中,多次打开关闭这个activity,然后看Profiler:

检测内存泄漏.png

多次操作后,dump一段此时的内存信息,通过包名查找可以看到内存中有5个SecondActivity对象,明显是不正常的,因为我们现在已经退出了这个页面,但是没看代码的情况下,一定能判定发生了内存泄露吗?不一定,因为内存泄漏只有强引用和软引用时会发生,这里也可能是弱引用,只是gc还没有扫到这块内存区域,没有回收它。这时光用Profiler已经无法继续辨别它的引用类型,MAT(Memory Analyzer tool)就排上用场了。
上图的左上方有个保存的按钮可以将这段内存信息保存到一个文件中:
保存的内存信息文件.png

但是这个文件是不能直接被MAT工具打开的

mat报错.png

需要用到另一个工具转化文件格式:hprof-conv;这个工具已经集成在Android SDK中了

hprof-conv.png

要用这个工具需要先给他配置环境变量


配置环境变量.png

配置完成测试一下

image.png

路径切换到之前保存的内存文件路径下,输入命令转换格式:
hprof-conv memory-20191023T160433.hprof 1.hprof

格式转换.png

生成新文件 1.hprof

image.png

最后再使用MAT打开1.hprof文件

mat.png

终于可以看到对象信息了,点开Histogram查看内存中所有对象信息

所有对象信息.png

搜索SecondActivity,筛选掉软弱虚引用

image.png
image.png

筛选后还剩下2个强引用的对象,打开详情我们从最后一行开始看引用关系,
SecondActivity --> this$0 -->callback --> next -->mMessages -->mQueue -->sUiHandler
其中 this$0 就是SecondActivity 中的匿名内部类 Runnable对象,在创建匿名内部类对象时,外部类的对象被当作参数传递进去,这里就持有了外部类的强引用;最后被sUiHandler引用,从命名上就能看出它是一个静态变量,生命周期和应用一致,我们关闭SecondActivity 到这里就发生了内存泄漏。

2.LeakCanary

使用MAT来分析内存问题,有一些门槛,会有一些难度,并且效率也不是很高,对于一个内存泄漏问题,可能要进行多次排查和对比才能找到问题原因。 为了能够简单迅速的发现内存泄漏,Square公司基于MAT开源了LeakCanary。
LeakCanary接入项目后,在app测试运行期间就可以检测到内存泄漏,并生成日志,查看起来非常方便,后面会单独写一篇LeakCanary使用及源码分析的文章。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351