一、内存泄漏的概念

Java虚拟机中某块内存(一般是对象)应当被回收,但是因为某些原因(一般是程序员水平不足)无法被垃圾回收器回收,就叫做内存泄漏。

注意无法回收不等于内存泄漏, 用逻辑学表示就是内存泄漏 无法回收,内存泄漏是无法回收的充分条件,无法回收是内存泄漏的必要条件。内存泄漏了肯定是无法回收,但是无法回收不一定就是内存泄漏。比如定义一个强引用,指向一个对象,这个对象到处都在用,它不能回收,但是这不叫做内存泄漏。一定是应该要回收的,但是错误的被引用导致无法回收才叫内存泄漏!

而且,内存泄漏有时间上的相对性。在某一段时间内,可能某个对象意外引用,导致GC无法回收,但是过了某个时间节点,可能对象又可以被回收。但是短时间的内存泄漏堆积,也有引发OOM的危险。

想要彻底弄明白内存泄漏,你需要补充一下以下几点知识

1) Java虚拟机及GC的原理
https://www.jianshu.com/p/d6b7681dd44c

记住几个重要的点:

# 垃圾回收器回收的目标是堆内存中的对象
# 对象回收的条件是对象不再被任何变量引用
# 静态变量本身不会被回收,静态变量所引用的对象是可以回收的。
# 静态变量存储在方法区,不在堆内存,所以不存在回收一说。其所在类,只是便于去访问这个静态变量而已。

2)匿名内部类

https://www.cnblogs.com/wuhenzhidu/p/anonymous.html

所谓匿名内部类,就是在父类Outter里new Inner(){},注意一定要有{},不一定非要复写内部类Inner 的方法。如果只是new Inner(),就不算作是内部类了。

# 匿名内部类持有外部类的强引用

二、内存泄漏的原因

堆内存中的长生命周期对象持有短生命周期对象的强/软引用,尽管短生命周期的对象已经不再需要了,但是长生命周期对象持有它的引用而导致不能被回收,这就是Java内存泄露的根本原因。

注意持有引用的方向,是长生命周期的对象持有短生命周期对象的引用,导致短生命周期对象无法回收。

所谓的长生命周期,首先我们会联想到static,其实一个无限循环Thread也算作是一个长的生命周期。

三、如何发现内存泄漏

参考博文 https://blog.csdn.net/u012760183/article/details/52068490

内存泄漏到一定极限,如果程序继续申请内存,内存不够用就会抛出OOM异常,导致程序崩溃。注意触发OOM的地方,不一定就是内存泄漏的地方,不要错误地把目标锁定在OOM的地方。发现内存泄漏可以运用一定的工具和手段:

1)Android Studio Profiler工具

https://blog.csdn.net/u010838555/article/details/96483705

如果反复测试程序某个过程,Totals只增不减,强制GC之后仍然没有下降,就说明内存泄漏了。 强制GC之后 dump出内存,如果发现本应该被回收的对象仍然在内存里,那就说明这个对象已经内存泄漏了。

2)Android-LeakCanary

https://www.jianshu.com/p/61860529ee1b

https://blog.csdn.net/wyh_healer/article/details/60961109


LeakCanary 大致框架和实现原理: https://blog.csdn.net/adarcy/article/details/82055945

四、常见的内存泄漏形式

遵循长生命周期引用短生命周期的原则定位

1.静态变量

静态变量(任意的类型)引用了对象,如果业务结束了不需要再使用这些对象,可以把变量清空(容器)或者直接将变量置为null。

2.内部类

1)非静态内部类

https://blog.csdn.net/qq_41991743/article/details/89370840

非静态内部类对象一定会持有其外部类对象的隐式引用

如果非静态内部类对象被某一个变量引用,那么这个内部类的外部类对象也会被这个变量引用。假如这个变量只想持有内部类对象的引用,但是却意外的持有了内部类的外部类对象的引用。如果这个变量为静态变量一直存在,不作清空处理,则认为外部类对象内存泄漏。

举个例子:

如果Activity里定义了一个内部类,定义了一个静态成员变量,静态成员变量引用了内部类对象,如果Activity销毁,但是静态成员变量没有清空,那么这个Activity就内存泄漏了。

ublic class StaticObjectTestActivity extends AppCompatActivity {
    private static final String TAG = "StaticObjectTestActivit";
    private static BigObj bigObjPub;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_static_object_test);
        System.out.println("I'm activity " + this);
        bigObjPub = new BigObj();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
//        bigObjPub = null;             //此处若将静态变量bigObjPub置为null,则gc可以回收。
    }

    class BigObj {

        private final byte[] bytes;

        public BigObj() {
            bytes = new byte[1024 * 1024 * 50];
        }

        @Override
        protected void finalize() throws Throwable {
            System.out.println("I'm finalize");
            super.finalize();
        }
    }

}

可以看到StaticObjectTestActivity退出后会有一次泄漏,就是因为内部的静态成员变量bigObjPub引用了StaticObjectTestActivity 的内部类BigObj。

非静态内部类本身不一定会导致外部类内存泄漏,一旦内部类对象被静态变量引用,或者被长生命周期的对象引用(如静态变量指向的对象等),或者其自身的生命周期很长(如new 一个Thread sleep很久)都会导致内存泄漏。

2)静态内部类

静态内部类正好是对付非静态内部类内存泄漏的克星,因为它不存在对外部类的隐式引用。它只是定义在外部类里,受外部类的访问限制。

3.单例模式导致的内存泄漏

"单例"其实就是一个静态变量引用的对象,这个对象的生命周期就特别长,如果对象里引用了某个对象(如Context)等,就会造成这个对象无法被回收。

4.静态方法传入对象(Context)

静态方法每调用一次,参数传入Context,但是静态方法所在的类不去声明static变量引用这个Context对象,如果Context每次都是新的对象,那么内存中这个Context对象个数就会一直累加。但是,这种情况,当GC发生时,这些对象可以被自动回收

#android常见的会引用Context的静态方法API

#Toast

Toast.makeText(this, "1", Toast.LENGTH_SHORT).show();

4.Handler导致Activity内存泄漏

很多人可能都听说过在Activity里使用Handler会导致内存泄漏,但是具体是什么情况下会导致内存泄漏?是在Activity里声明Handler就会导致内存泄漏吗,答案是NO。Handler内存泄漏的本质原因是Handler被引用了,而往往这些引用不明显。

参照https://blog.csdn.net/xzw00/article/details/50156471 我写了几个Demo测试了一下

#1)Handler被MessageQueue引用导致内存泄漏

public class HandlerLeakTestActivity extends AppCompatActivity {
    private static final String TAG = "HandlerLeakTestActivity";
    protected static final int MSG_TEST = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_leak_test);
        Log.d(TAG, "onCreate: " + "I'm activity " + this);
        mHandler.sendEmptyMessage(MSG_TEST);
        finish();
    }

    private  Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // TODO Auto-generated method stub
            switch (msg.what) {
                case MSG_TEST:
                    Log.w(TAG, "Handle the test message!");
                    mHandler.sendEmptyMessageDelayed(MSG_TEST, 5000);
                    break;

                default:
                    break;
            }
            super.handleMessage(msg);
        }
    };

}

不断的开启关闭HandlerLeakTestActivity,然后强制GC,发现
多个HandlerLeakTestActivity 及Handler对象仍然驻留在内存中。

把mHandler.sendEmptyMessageDelayed(MSG_TEST, 5000);这行代码去掉,再
开启关闭HandlerLeakTestActivity , 发现多个HandlerLeakTestActivity 及Handler对象驻留在内存中。 但是强制GC,这些对象可以自动回收。也就是说handler的postDelay方法会导致Activity内存泄漏,在 https://www.cnblogs.com/aimr/p/5217918.html 这篇文章说明的很清楚:

如果你执行了Handler的postDelayed()方法,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。

上面的作者阐述的观点: handler的postDelay方法会导致Activity内存泄漏 有一定的片面性,上面

 case MSG_TEST:
                    Log.w(TAG, "Handle the test message!");
                    mHandler.sendEmptyMessageDelayed(MSG_TEST, 5000);
                    break;

会导致Handler不停的发送MSG_TEST这个消息,从而形成
MessageQueue -> Message -> Handler -> Activity的链 ,导致Activity无法被回收。

对上面的代码做一下改动:

#改动1:让发送到一定数目消息后,停止消息的发送,发现停止消息发送之后,强制GC可以回收Activity及Handler对象。

public class InnerHandlerTestActivity extends AppCompatActivity {
    private static final String TAG = "InnerHandlerTestAct";
    protected static final int MSG_TEST = 1;
    private int sendCount = 0;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple_handler_test);
        Log.d(TAG, "onCreate: " + "I'm activity " + this);
        mHandler.sendEmptyMessage(MSG_TEST);
        finish();
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // TODO Auto-generated method stub
            switch (msg.what) {
                case MSG_TEST:
                    if(sendCount > 10){
                        return;
                    }
                    Log.w(TAG, "Handle the test message!");
                    mHandler.sendEmptyMessageDelayed(MSG_TEST, 1000);
                    sendCount++;
                    break;

                default:
                    break;
            }
            super.handleMessage(msg);
        }
    };
    
}

所以,Handler不停地发送消息让MessageQueue 一直持有Handler引用,导致内存泄漏。

#改动2: 就用sendEmptyMessageDelayed稍微延时长点发个消息,发现只有过了延时时间30s之后才能强制GC回收Activity及Handler对象。

public class InnerClassHandlerTestActivity extends AppCompatActivity {
    private static final String TAG = "InnerHandlerTestAct";
    protected static final int MSG_TEST = 1;
    private int sendCount = 0;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple_handler_test);
        Log.d(TAG, "onCreate: " + "I'm activity " + this);
        mHandler.sendEmptyMessageDelayed(MSG_TEST,30 * 1000);
        finish();
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // TODO Auto-generated method stub
            switch (msg.what) {
                case MSG_TEST:
                    break;

                default:
                    break;
            }
            super.handleMessage(msg);
        }
    };

}

所以,Handler只要消息没有处理完,就会导致内存泄漏。

#2)Handler被Thread引用导致内存泄漏

下面是Android惯用的刷新UI的写法,在子线程里做耗时操作,然后通过handler发送消息去刷新UI。

public class ThreadHandlerTestActivity extends AppCompatActivity {
    private static final String TAG = "InnerHandlerTestAct";
    protected static final int MSG_TEST = 1;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread_handler_test);
        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(30*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mHandler.sendEmptyMessage(MSG_TEST);
            }
        }.start();
        finish();
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // TODO Auto-generated method stub
            switch (msg.what) {
                case MSG_TEST:
                    Log.w(TAG, "Handle the test message!");
//                    mHandler.sendEmptyMessageDelayed(MSG_TEST, 5000);
                    break;

                default:
                    break;
            }
            super.handleMessage(msg);
        }
    };
}

Thread 30秒的耗时操作,虽然最后才调用mHandler.sendEmptyMessage(MSG_TEST),但是引用是自Thread创建就开始,到Thread的run方法执行完毕,Thread销毁之后引用才结束。所以在30秒以内,强制GC,Activity、Handler不会被回收。

#3)非内部类的Handler被Thread引用也会导致内存泄漏

如上图,我把Handler单独定义成一个类,不定义在Activity内部。30s之内,MyPubHandler仍然会持有Activity,强制GC也无法回收,但是过了30s之后,强制GC就能将Activity和Handler回收。

什么情况?非匿名内部类的Handler也会导致Activity内存泄漏?仔细看handler的postDelayed方法,创建了一个Runnable的匿名对象,形成了MessageQueue -> Message -> Handler ->Runnable-> Activity的链 。

那么我换个handler的消息方法

这样,30s之内,强制GC会回收Activity,30之后,Handler消息处理完才能能回收。

所以,Handler的内存泄漏应该分为2段来理解:

1)Handler存在待处理的消息,会有 MessageQueue -> Message -> Handler 的引用,导致Handler无法回收。

2)Handler如果定义成匿名内部类或者post产生了一个匿名Runnable,如果Handler无法回收,会持有Activity的引用,导致Activity也无法回收。

#Handler内存泄漏的解决办法

1)最简单有效且最正规的办法就是在Activity的onDestroy方法里调用

mHandler.removeCallbacksAndMessages(null);

但是上面这个方法也不一定完全能保证Handler可以被回收,比如:

handler消息里又搞了一个匿名内部类Thread,Thread永远不会结束,即使调用了mHandler.removeCallbacksAndMessages(null);也避免不了Handler内存泄漏。

GC之后,发现Thread->Handler->Activity,导致无法回收。

2)如果Handler非要定义成匿名内部类,可以定义成静态变量。

#静态的Handler用完之后,记得要置为null,否则这个Handler实例会常驻内存,GC也无法回收,直到应用程序被杀死。Handler置为null,GC也不会立马被回收,只有Handler的消息全部处理完毕才会回收。

#Handler定义成static了,那么它更新View的时候,View启不是也要定义成static?

https://www.cnblogs.com/punkisnotdead/p/4943260.html

上面这篇文章提到了将activity通过弱引用的方式传入到静态的Handler中


0 条评论

发表回复

您的电子邮箱地址不会被公开。