【从 0 开始开发一款直播 APP】8 弱引用 WeakReference

本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类"腾讯直播"的商业化项目
视频地址:http://www.cniao5.com/course/10121


先来一则关于「攻城狮们对弱引用认知度」相关的调查,如下是 「技术小黑屋」对于「Java高级开发工程师的应聘者对弱引用的认知度」的一些看法,看到此,我很是吃惊。

「不久之前,我面试了一些求职 Java 高级开发工程师的应聘者。我常常会面试他们说,“你能给我介绍一些 Java 中得弱引用吗?”,如果面试者这样说,“嗯,是不是垃圾回收有关的?”,我就会基本满意了,我并不期待回答是一篇诘究本末的论文描述。

然而事与愿违,我很吃惊的发现,在将近 20 多个有着平均 5 年开发经验和高学历背景的应聘者中,居然只有两个人知道弱引用的存在,但是在这两个人之中只有一个人真正了解这方面的知识。在面试过程中,我还尝试提示一些东西,来看看有没有人突然说一声“原来是这个啊”,结果很是让我失望。我开始困惑,为什么这块的知识如此不被重视,毕竟弱引用是一个很有用途的特性,况且这个特性已经在 7 年前 Java 1.2 发布时便引入了。」

很庆幸,在我还没从事工作之前就知道了弱引用,并有幸学习它,心里窃喜。

一、Java 引用介绍

从 java 1.2 版本引入 java.lang.ref 包,共 4 种引用,这 4 种引用的级别高低依次为:

强引用 > 软引用 > 弱引用  > 虚引用

1.1、强引用 Strong Reference

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

强引用就是我们常使用到的引用,写法如下:

A a = new A();
B b = new B(a);

上面两个强引用就这样产生了,并且 a 是对象 A 的引用,b 是对象 B 的引用,而 B 还依赖于 A,那么就认为 B 是可以到达 A 的。

A a = new A();
B b = new B(a);
a = null;

当把 a = null 时,这时 a 不再指向 A 的地址,按道理:当某个对象不再被其它对象引用的时候,会被 GC 回收,而 a = null 时,A 对象不能被回收,B 还依赖于 A,造成了内存泄漏。

强引用最重要的就是它能够让引用变强,这就决定了它和GC的交互,如果一个对象通过强引用链接可到达,它就不会被 GC 回收。当然,如果你不想让正在使用的对象被回收,这正是强引用的强大之处。

看到上述示例强引用带来的内存泄漏问题,我们要如何避免?

1.2、弱引用 Weak Reference

弱引用就是将对象留在内存的能力弱于强引用,使用 WeakReference,垃圾回收器会帮你来决定引用的对象何时回收并且将对象从内存移除。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

A a = new A();
WeakReference b = new WeakReference(a);
//B b = new B(a);
a = null;

当 a = null 时,这个时候 A 只被弱引用依赖,GC 回立刻回收 A 对象,这就是弱引用的好处,避免内存泄漏。

1.3、引用队列 ReferenceQueue

引用队列 ReferenceQueue 配合 Reference 子类等使用,当引用对象所指向的内存空间被 GC 回收后,该引用对象则被追加到引用队列的末尾。根据下面的代码,说明只供 Reference 实例调用,且只能调用一次。

    /**
     * Enqueue the reference object on the receiver.
     * @param reference reference object to be enqueued.
     * @return true if the reference was enqueued.
     */
boolean enqueue(Reference reference) {
    synchronized (lock) {
        if (enqueueLocked(reference)) {
           lock.notifyAll();
           return true;
        }
        return false;
    }
}

引用队列有如下实例方法

当队列中出队一个元素时,若队列为空返回 null,否则返回队列。

/**
 * Polls this queue to see if a reference object is available.  If one is
 * available without further delay then it is removed from the queue and
 * returned.  Otherwise this method immediately returns null.
 * @return  A reference object, if one was immediately available,
 *          otherwise null
 */
public Reference poll() {
    synchronized (lock) {
        if (head == null)
            return null;
        return reallyPollLocked();
    }
}
//入队
private Reference reallyPollLocked() {
        if (head != null) {
            Reference r = head;
            if (head == tail) {
                tail = null;
                head = null;
            } else {
                head = head.queueNext;
            }

            // Update queueNext to indicate that the reference has been
            // enqueued, but is now removed from the queue.
            r.queueNext = sQueueNextUnenqueued;
            return r;
        }
        return null;
}

从队列中出队一个元素,若没有则阻塞直到有元素可出队或超过 timeout 指定的毫秒数(由于采用 wait(long timeout) 方式实现等待,因此时间不能保证)。

public Reference remove(long timeout)
    throws IllegalArgumentException, InterruptedException
{
    if (timeout < 0) {
        throw new IllegalArgumentException("Negative timeout value");
    }
    synchronized (lock) {
        Reference r = reallyPollLocked();
        if (r != null) return r;
        long start = (timeout == 0) ? 0 : System.nanoTime();
        for (;;) {
            lock.wait(timeout);
            r = reallyPollLocked();
            if (r != null) return r;
            if (timeout != 0) {
                long end = System.nanoTime();
                timeout -= (end - start) / 1000_000;
                if (timeout <= 0) return null;
                start = end;
            }
        }
    }
}

1.4、软引用 SoftReference

软引用阻止 GC 回收其指向对象的能力要强一些。通过 SoftReference 表示, 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

当 JVM 中的内存不足的时候,垃圾回收器会释放那些只被软引用所指向的对象。如果全部释放完这些对象之后,内存还不足,才会抛出 OutOfMemoryError。软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。比如考虑一个图像编辑器的程序。该程序会把图像文件的全部内容都读取到内存中,以方便进行处理。而用户也可以同时打开多个文件。当同时打开的文件过多的时候,就可能造成内存不足。如果使用软引用来指向图像文件内容的话,垃圾回收器就可以在必要的时候回收掉这些内存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,JVM 就会把这个软引用加入到与之关联的引用队列中。

软引用与弱引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

1.5、虚引用 Phantom Reference

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。我们可以通过 get 方法来得到其指定对象,它的唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。虚引用职能在其指向的对象从内存中移除掉之后才会加入到引用队列。其 get 方法一直返回 null 就是为了阻止其指向的几乎被销毁的对象重新复活。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

ReferenceQueue queue = new ReferenceQueue (); 
PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

虚引用使用场景主要有两个。

1、它允许你知道具体何时其引用的对象从内存中移除。而实际上这是Java中唯一的方式。这一点尤其表现在处理类似图片的大文件的情况。当你确定一个图片数据对象应该被回收,你可以利用虚引用来判断这个对象回收之后在继续加载下一张图片。这样可以尽可能地避免可怕的内存溢出错误。

2、虚引用可以避免很多析构时的问题。finalize 方法可以通过创建强引用指向快被销毁的对象来让这些对象重新复活。然而,一个重写了 finalize 方法的对象如果想要被回收掉,需要经历两个单独的垃圾收集周期。在第一个周期中,某个对象被标记为可回收,进而才能进行析构。但是因为在析构过程中仍有微弱的可能这个对象会重新复活。这种情况下,在这个对象真实销毁之前,垃圾回收器需要再次运行。因为析构可能并不是很及时,所以在调用对象的析构之前,需要经历数量不确定的垃圾收集周期。这就意味着在真正清理掉这个对象的时候可能发生很大的延迟。这就是为什么当大部分堆被标记成垃圾时还是会出现烦人的内存溢出错误。

引用总结

级别 何时被 GC 回收 生存时间
强引用 从来不会 对象的一般状态 JVM 停止运行时终止
软引用 在内存不足时 缓存内存不足终止
弱引用 对象未被引用时 缓存GC 运行后终止
虚引用 对象被回收时 内存使用控制 引用队列中对象被回收

二、CountDownTimer 中使用到弱引用讲解

上文中,在倒计时 Demo 中,用到了弱引用,看完弱引用之后,你应该对弱引用有了大致了解,那么下面针对上一章内容,我们再回过头来看看为什么要用弱引用吧。我们是要用 TextView 来实现倒计时功能,如果直接用强引用,TextView 在被其它对象引用不能及时回收时,可能会造成内存泄漏。

/**
 * 在按钮上启动一个定时器
 * @param tvVerifyCode  验证码控件
 * @param defaultString 按钮上默认的字符串
 * @param max           失效时间(单位:s)
 * @param interval      更新间隔(单位:s)
 */
public static void startTimer(final WeakReference tvVerifyCode,
                       final String defaultString,
                       int max,
                       int interval) {
   tvVerifyCode.get().setEnabled(false);
   new CountDownTimer(max * 1000, interval * 1000 - 10) {
      @Override
      public void onTick(long time) {
         if (null == tvVerifyCode.get())
            this.cancel();
         else
            tvVerifyCode.get().setText("" + ((time + 15) / 1000) + "s");
      }

      @Override
      public void onFinish() {
         if (null == tvVerifyCode.get()) {
            this.cancel();
            return;
         }
         tvVerifyCode.get().setEnabled(true);
         tvVerifyCode.get().setText(defaultString);
      }
   }.start();
}

参考:
http://www.cnblogs.com/fsjohnhuang/p/4268411.html
http://blog.csdn.net/matrix_xu/article/details/8424038
http://www.cnblogs.com/skywang12345/p/3154474.html
http://droidyue.com/blog/2014/10/12/understanding-weakreference-in-java/
http://www.infoq.com/cn/articles/cf-java-garbage-references

【五一大促】菜鸟窝全场android项目实战课程低至五折,更有价值33元的四款热门技术免费领,17年初优惠力度最大的一次活动,有意向的童鞋不要错过
狂戳>>http://www.cniao5.com/hd/2017/51.html

你可能感兴趣的:(【从 0 开始开发一款直播 APP】8 弱引用 WeakReference)