Android:Handler 二三事(二)由内存泄漏所想到的(垃圾回收机制)

主要内容

解决Handler内存泄漏以及延伸(垃圾回收、引用等)

解决Handler内存泄漏及延伸

为什么Handler会引起内存泄漏?

这是一段使用Handler的代码

public class LeakHandlerActivity extends AppCompatActivity {

    private Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        myHandler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        },1000 * 60 * 3);
    }

}

首先lint会给出提示

展开全部,可以看到

Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.

分析一下大意:

在Java中,非静态内部类和匿名内部类会持有外部类的引用,因此可能会阻止外部类的垃圾回收。

如果程序在主线程以外的线程使用Looper或者MessageQueue,没有问题。但是如果在主线程,则需要对Handler做处理;

  1. 将Handler声明为静态类或者新写一个Java文件用一个类继承Handler
  2. 因为是静态类,不持有外部类的引用,如果有对外部类的引用,使用WeakReference ,也就是弱引用。

这里涉及到几个问题:

  1. Java的垃圾回收是如何工作的,怎么就阻止垃圾回收了
  2. 为什么主线程中需要对Handler做处理
  3. 为什么使用弱引用

Java的垃圾回收机制

Java相对于C++,它的垃圾回收是自动的,不需要写专门的代码去释放垃圾。

为什么需要垃圾回收

因为内存空间是有限的,如果一直为新对象分配内存空间,而不释放的话,内存就会承载不了,就会造成OOM异常。

什么是垃圾回收机制

顾名思义,就是释放垃圾所占的空间。那么就存在几个问题:

  • 怎么判定某个对象是垃圾
  • 怎么回收
  • 用什么回收
怎么判定某个对象是垃圾?

可达性分析法。超出作用域就是不可达的,在作用域内就是可达的。这个算法是从通过一系列称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索过的路径就是引用链。当一个对象到GC Roots没有任何引用链的话,那个这个对象就是不可达的,也就是不可用的。这里需要注意的是,这个对象是从Root搜索不到,并且经过第一次标记、清理后,仍然没有复活的对象。

如何选取“GC Roots”对象呢,Java里,一般可以作为Root对象的有这几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法咋栈中JNI(native方法)引用的对象

这几个跟JVM(Java虚拟机)运行时数据区有关系。它分为5个部分:程序计数器,Java栈、本地方法栈、方法区、堆

  • 程序计数器:虽然他不像汇编中的程序计数器一样时物理概念上的CPU寄存器,但是功能是一样的,都是用来指示执行哪条指令的。在JVM中,多线程中是轮流切换着被CPU执行的,一个CPU在某个时刻只会执行一条线程中的指令。为了能保证线程在切换后能回复到切换前的执行位置,每个线程都需要有自己独立的程序计数器,而且不能被相互干扰,所以他们是每个线程都有的,并且是私有的。它的大小不会随着程序的执行而改变。
  • Java栈(又叫栈,虚拟机栈):里面存放的是一个个栈帧,每个栈帧对应着一个被调用的方法,里面有一个局部变量表,用来存储方法中的局部变量。如果是基本数据类型就存储值,如果引用类型的变量,就存储对象的引用。它在编译骑就确定其大小了。每个线程都有自己的栈,并且会不干扰。
  • 本地方法栈:和Java栈类似,区别是Java栈是为Java方法服务的,本地方法栈是为本地方法(Native Method)服务的。
  • 方法区:和堆一样,是线程共享区域。它存储的是类信息,静态变量,常量,以及编译器编译后的代码等。Java规范中,没有要求方法区必须实现垃圾回收
  • 堆:唯一一个程序员可以管理的区域。Java堆中是用来存储对象本身以及数组(数组引用是存放在栈中)。JVM中只有一个栈,是所有线程共享的。Java中有垃圾回收机制来管理这块区域。

引用记数法也是判断对象是不是垃圾的方式之一,当对象被引用时,计数器+1,失效时,计数器-1。也即是说,当计数器为0的时候对象就是不可能再被使用了。但是Java中没有采用这种引用方式。因为它无法处理相互引用。那么什么是相互引用呢?

public class TestA {
    public TestB mTestB;
}
public class TestB {
    public TestA mTestA;
}

类A和类B各自持有一个对方类的对象

public class TestGC {

    public static void mian(String[] args) {
        TestA mTestA = new TestA();
        TestB mTestB = new TestB();

        mTestA.mTestB = mTestB;
        mTestB.mTestA = mTestA;

        mTestA = null;
        mTestB = null;
    }
}

即便最后都已经置null了,但是他们还是相互引用着,所以引用计数器还是标记为1。这就是问题,所以目前主流是用可达性分析法。

怎么回收

这里需要了解的是Java的内存模型。

JVM主要管理两种内存模型:堆和非堆。

  • 堆上面有讲到,主要为类实例和数据分配内存
  • 非堆是存放类加载信息等等,它是JVM堆之外的内存。
  • 还有一个Other,存放JVM自身代码。


堆分为三个代:年轻代,年老代,持久代。


  • 年轻代(Young Gerneration):所有新生成的对象,分为Eden和两个Servivor区域。
  • 年老代(Old Generation):经历多次垃圾回收还存放的区域,或者是一些生命周期比较长的对象
  • 持久代(Perm):用于存放静态文件,如静态类,方法等。

一次申请内存的过程:

  1. JVM会为Java对象在Eden中初始化一块内存区域
  2. 如果内存足够,OK,申请结束;
  3. 如果不够,JVM会试图释放在Eden中所有不活跃的对象(minor gc)。释放后如果还不够,那么就就试图将Eden中部分活跃的对象放到Servivor区。
  4. Servivor区用来作为Young和Old区的交换区域。如果Old区的空间足够,那么Servivor的对象将会移到Old区,否则留在Servivor区。
  5. 当Old区空间不够时,将会进行完全的垃圾收集(full gc)
  6. 如果完全垃圾收集后,如果Servivor区以及Old区仍然无法存放从Eden复制过来的对象,导致JVM无法为新对象分配内存区域,那么就出现OOM

从中我们可以看到垃圾回收的时机,以及OOM产生的原因。

年轻代回收使用的的时复制算法,年老代回收使用的时标记算法。

复制算法:先把所有的对象分配到from区域,清理时将所有活动对象复制到to区域,然后清除from区域。然后from区域和to区域互换。每次清理重复这个过程。

标记算法:标记出所有可以被回收的对象,并且清理空间。

  • 标记算法的好处是容易实现,但是容易产生内存碎片,导致下一次回收提前;
  • 复制算法的好处是高效而且不容易产生碎片,但是内存空间代价高,因为能够使用的内存缩减了一半。而且存活对象很多,那么复制算法的效率就会降低
用什么回收

垃圾回收器。如果回收算法是理论基础,那么回收器就是具体实现。

  • Serial/Serial Old:最古老的收集器。单线程收集器,如果进行垃圾收集时,必须暂停所有用户线程。Serial采用复制算法,针对年轻代,Serial Old采用标记算法,针对年老代。它的优点是简单高效,但是会对用户带来停顿。
  • ParNew:Serial收集器的多线程版本,使用多个线程进行垃圾收集。
  • Parallel Scavenge 年轻代的多线程收集器。回收期间不需要暂停其他线程。采用复制算法,目的是达到一个可控的吞吐量。
  • Parallel Old: 上一个的年老代版本,使用多线程和标记算法。
  • CMS:以获取最短回收停顿时间为目标的并发收集器,采用标记算法。
  • G1:最前沿的成果,并发,面向服务端,能建立可预测的停顿时间模型

为什么主线程中需要对Handler做处理

当一个程序启动的时候,FrameWork自动为这个应用程序的主线程新建一个Looper,这个Loope关联管着所有主要的框架时事件,比如Activity生命周期,点击事件等,这些事件都是一个个消息对象,它一直在循环处理消息对象。它存在与应用的生命周期中。

如果Handler在主线程中初始化,就会与Looper关联,消息发送到Looper的消息队列中时,会有一个Handler的引用,以便处理消息时可以调用handleMessage方法。

当Activity被销毁时,因为延时消息会在被处理之前在主线程的消息队列中有段时间,这个消息还保留着Handler的引用,Handler因为时匿名内部类又保留着Activity的实例,这些引用会一直保持到消息被处理,从而导致Activity暂时无法被回收,Activity持有的资源也无法回收,造成内存泄漏。

为什么使用弱引用

Java的四种引用类型:强引用,软引用,弱引用,虚引用。好处是可以管理对象的生命周期,便于垃圾回收。

强引用:实例化一个对象。强引用只要不为null,有引用变量,那么就永远不会被垃圾回收。

 TestA a = new TestA();
 String b = "blabla";

软引用:如果内存足够,不会回收;如果内存不够,就会回收。常用于图片缓存,网络缓存等。

SoftReference<TestA> s = new SoftReference<>(a);

弱引用:无论内存是否充足,;垃圾回收器检测到时,都会被回收。

 WeakReference<TestA> w = new WeakReference<>(a);

虚引用:随时可能被回收。

解决内存泄漏

由上总结,我们需要做的,静态内部类,弱引用。

    /**
     * 静态内部类
     */
    private static class MyHandler extends Handler {

        private final WeakReference<Activity> activity;

        public MyHandler(Activity activity) {
            this.activity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            Activity useActivity = activity.get();
            if(useActivity != null) {
                // do something
            }
        }
    }

当然,我们也可以封装起来,以便复用。

public class BaseHandler<T> extends Handler {

    private final WeakReference<T> weak_reference;
    
    public BaseHandler(T t) {
        weak_reference = new WeakReference<>(t);
    }

    @Override
    public void handleMessage(Message msg) {
        T activity = weak_reference.get();
        if (activity != null) {
            handleMessageWeakActivity(msg, activity);
        }
    }

    public void handleMessageWeakActivity(Message msg, T t) {

    }
}

参考

https://www.cnblogs.com/dolphin0520/p/3613043.html

http://icyfenix.iteye.com/blog/715301

http://www.cnblogs.com/dolphin0520/p/3783345.html

https://blog.csdn.net/ithomer/article/details/6252552

http://ifeve.com/jvm-yong-generation/

https://blog.csdn.net/cpcpcp123/article/details/51262940

https://www.cnblogs.com/xiaoxi/p/6486852.html

https://blog.csdn.net/lqw_student/article/details/52954837

https://blog.csdn.net/swebin/article/details/78571933

感谢@star

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值