java 源码系列 - 带你读懂 Reference 和 ReferenceQueue

1,514 阅读16分钟
原文链接: mp.weixin.qq.com

从基础讲起

Reference

主要是负责内存的一个状态,当然它还和java虚拟机,垃圾回收器打交道。Reference类首先把内存分为4种状态Active,Pending,Enqueued,Inactive。

  • Active,一般来说内存一开始被分配的状态都是 Active,

  • Pending 大概是指快要被放进队列的对象,也就是马上要回收的对象,

  • Enqueued 就是对象的内存已经被回收了,我们已经把这个对象放入到一个队列中,方便以后我们查询某个对象是否被回收,

  • Inactive就是最终的状态,不能再变为其它状态。

ReferenceQueue

引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到队列中,ReferenceQueue实现了入队(enqueue)和出队(poll),还有remove操作,内部元素head就是泛型的Reference。

简单例子

当我们想检测一个对象是否被回收了,那么我们就可以采用 Reference + ReferenceQueue,大概需要几个步骤:

  • 创建一个引用队列 queue

  • 创建 Refrence 对象,并关联引用队列 queue

  • 在 reference 被回收的时候,refrence 会被添加到 queue 中

 1创建一个引用队列   2ReferenceQueue queue = new ReferenceQueue();   3 4// 创建弱引用,此时状态为Active,并且Reference.pending为空,当前Reference.queue = 上面创建的queue,并且next=null   5WeakReference reference = new WeakReference(new Object(), queue);   6System.out.println(reference);   7// 当GC执行后,由于是弱引用,所以回收该object对象,并且置于pending上,此时reference的状态为PENDING   8System.gc();   910/* ReferenceHandler从pending中取下该元素,并且将该元素放入到queue中,此时Reference状态为ENQUEUED,Reference.queue = ReferenceENQUEUED */  1112/* 当从queue里面取出该元素,则变为INACTIVE,Reference.queue = Reference.NULL */  13Reference reference1 = queue.remove();  14System.out.println(reference1);

那这个可以用来干什么了?

可以用来检测内存泄露, github 上面 的 leekCanary 就是采用这种原理来检测的。

  • 监听 Activity 的生命周期

  • 在 onDestroy 的时候,创建相应的 Refrence 和 RefrenceQueue,并启动后台进程去检测

  • 一段时间之后,从 RefrenceQueue 读取,若读取不到相应 activity 的 Refrence,有可能发生泄露了,这个时候,再促发 gc,一段时间之后,再去读取,若在从 RefrenceQueue 还是读取不到相应 activity 的 refrence,可以断定是发生内存泄露了

  • 发生内瘘泄露之后,dump,分析 hprof 文件,找到泄露路径


Refrence

主要内存成员变量

 1private T referent;        2volatile ReferenceQueue<? super T> queue;   3 4/* When active:   NULL  5 *     pending:   this  6 *    Enqueued:   next reference in queue (or this if last)  7 *    Inactive:   this  8 */   9@SuppressWarnings("rawtypes")  10Reference next;  111213transient private Reference<T> discovered;  /* used by VM */  141516/* List of References waiting to be enqueued.  The collector adds17 * References to this list, while the Reference-handler thread removes18 * them.  This list is protected by the above lock object. The19 * list uses the discovered field to link its elements.20 */21private static Reference<Object> pending = null;
  • referent表示其引用的对象,即在构造的时候需要被包装在其中的对象。

  • queue 是对象即将被回收时所要通知的队列。当对象即将被回收时,整个reference对象,而不仅仅是被回收的对象,会被放到queue 里面,然后外部程序即可通过监控这个 queue 即可拿到相应的数据了。

  • next 即当前引用节点所存储的下一个即将被处理的节点。但 next 仅在放到queue中才会有意义,因为只有在enqueue的时候,会将next设置为下一个要处理的Reference对象。为了描述相应的状态值,在放到队列当中后,其queue就不会再引用这个队列了。而是引用一个特殊的 ENQUEUED(内部定义的一个空队列)。因为已经放到队列当中,并且不会再次放到队列当中。

  • discovered 表示要处理的对象的下一个对象。即可以理解要处理的对象也是一个链表,通过discovered进行排队,这边只需要不停地拿到pending,然后再通过discovered 不断地拿到下一个对象赋值给pending即可,直到取到了最有一个。它是被JVM 使用的。

  • pending 是等待被入队的引用列表。JVM 收集器会添加引用到这个列表,直到Reference-handler线程移除了它们。这个列表使用 discovered 字段来连接它下一个元素(即 pending 的下一个元素就是discovered对象。r = pending; pending = r.discovered)。

接下来,我们来看一下 Refrence 的静态代码块

 1static { 2    ThreadGroup tg = Thread.currentThread().getThreadGroup(); 3    for (ThreadGroup tgn = tg; 4         tgn != null; 5         tg = tgn, tgn = tg.getParent()); 6    Thread handler = new ReferenceHandler(tg, "Reference Handler"); 7    /* If there were a special system-only priority greater than 8     * MAX_PRIORITY, it would be used here 9     */10    handler.setPriority(Thread.MAX_PRIORITY);11    handler.setDaemon(true);12    handler.start();1314    // provide access in SharedSecrets15    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {16        @Override17        public boolean tryHandlePendingReference() {18            return tryHandlePending(false);19        }20    });21}

我们,当 Refrence 类被加载的时候,会执行静态代码块。在静态代码块里面,会启动 ReferenceHandler 线程,并设置线程的级别为最大级别, Thread.MAX_PRIORITY。

接下来我们来看一下 ReferenceHandler 这个类,可以看到 run 方法里面是一个死循环,我们主要关注 tryHandlePending 方法就 Ok 了

 1private static class ReferenceHandler extends Thread { 2 3   ----- // 核心代码如下 4 5    public void run() { 6        while (true) { 7            tryHandlePending(true); 8        } 9    }10}
 1static boolean tryHandlePending(boolean waitForNotify) { 2    Reference<Object> r; 3    Cleaner c; 4    try { 5        synchronized (lock) { 6            // 检查 pending 是否为 null,不为 null,制定 pending enqueue 7            if (pending != null) { 8                r = pending; 9                // 'instanceof' might throw OutOfMemoryError sometimes10                // so do this before un-linking 'r' from the 'pending' chain...11                c = r instanceof Cleaner ? (Cleaner) r : null;12                // unlink 'r' from 'pending' chain13                pending = r.discovered;14                r.discovered = null;15            } else { // 为 null。等待16                // The waiting on the lock may cause an OutOfMemoryError17                // because it may try to allocate exception objects.18                if (waitForNotify) {19                    lock.wait();20                }21                // retry if waited22                return waitForNotify;23            }24        }25    } catch (OutOfMemoryError x) {26        // Give other threads CPU time so they hopefully drop some live references27        // and GC reclaims some space.28        // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above29        // persistently throws OOME for some time...30        Thread.yield();31        // retry32        return true;33    } catch (InterruptedException x) {34        // retry35        return true;36    }3738    // Fast path for cleaners39    if (c != null) {40        c.clean();41        return true;42    }4344    ReferenceQueue<? super Object> q = r.queue;45    if (q != ReferenceQueue.NULL) q.enqueue(r);46    return true;47}

在 tryHandlePending 方法里面,检查 pending 是否为 null,如果pending不为 null,则将 pending 进行 enqueue,否则线程进入 wait 状态。

问题来了,我们从 Reference 源码中发现没有给 discovered和 pending 赋值的地方,那 pending和 discovered 到底是谁给他们赋值的。

我们回头再来看一下注释:简单来说,垃圾回收器会把 References 添加进入,Reference-handler thread 会移除它,即 discovered和 pending 是由垃圾回收器进行赋值的

1/* List of References waiting to be enqueued.  The collector adds2 * References to this list, while the Reference-handler thread removes3 * them.  This list is protected by the above lock object. The4 * list uses the discovered field to link its elements.5 */6private static Reference<Object> pending = null;

RefrenceQueue

接下来,我们在来看一下 RefrenceQueue 的 enqueue 方法

 1boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */ 2    synchronized (lock) { 3        // Check that since getting the lock this reference hasn't already been 4        // enqueued (and even then removed) 5        ReferenceQueue<?> queue = r.queue; 6        // queue 为 null 或者 queue 已经被回收了,直接返回 7        if ((queue == NULL) || (queue == ENQUEUED)) { 8            return false; 9        }10        assert queue == this;11        // 将 refrence 的状态置为 Enqueued,表示已经被回收12        r.queue = ENQUEUED;13        // 接着,将 refrence 插入到链表14        // 判断当前链表是否为 null,不为 null,将 r.next 指向 head,为 null,head 直接指向 r15        r.next = (head == null) ? r : head;16        // head 指针指向 r17        head = r;18        queueLength++;19        if (r instanceof FinalReference) {20            sun.misc.VM.addFinalRefCount(1);21        }22        lock.notifyAll();23        return true;24    }25}
  • 判断 queue 为 null 或者 queue 已经被回收了,直接返回

  • 若 queue 不为 null,将 r (refrence) 的状态置为 Enqueued,表示已经被回收

  • 将 refrence 插入到 queue 的头部

Refrence 和 RefrenceQueue 的源码分析到此为止


Refrence 的子类

4种引用我们都知道在Java中有4种引用,这四种引用从高到低分别为:

1) StrongReference

这个引用在Java中没有相应的类与之对应,但是强引用比较普遍,例如:Object obj = new Object();这里的obj就是要给强引用,如果一个对象具有强引用,则垃圾回收器始终不会回收此对象。当内存不足时,JVM情愿抛出OOM异常使程序异常终止也不会靠回收强引用的对象来解决内存不足的问题。

2) SoftReference

如果一个对象只有软引用,则在内存充足的情况下是不会回收此对象的,但是,在内部不足即将要抛出OOM异常时就会回收此对象来解决内存不足的问题。

 1public class TestSoftReference { 2        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>(); 3        public static void main(String[] args){ 4            Object obj = new Object(); 5            SoftReference<Object> sf = new SoftReference(obj,rq); 6            System.out.println(sf.get()!=null); 7            System.gc(); 8            obj = null; 9            System.out.println(sf.get()!=null);1011        }12    }

运行结果均为:true。

这也就说明了当内存充足的时候一个对象只有软引用也不会被JVM回收。

3) WeakReference

WeakReference 基本与SoftReference 类似,只是回收的策略不同。

只要 GC 发现一个对象只有弱引用,则就会回收此弱引用对象。但是由于GC所在的线程优先级比较低,不会立即发现所有弱引用对象并进行回收。只要GC对它所管辖的内存区域进行扫描时发现了弱引用对象就进行回收。

看一个例子:

 1public class TestWeakReference { 2        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>(); 3        public static void main(String[] args) { 4            Object obj = new Object(); 5            WeakReference<Object> wr = new WeakReference(obj,rq); 6            System.out.println(wr.get()!=null); 7            obj = null; 8            System.gc(); 9            System.out.println(wr.get()!=null);//false,这是因为WeakReference被回收10        }1112    }

运行结果为: true 、false

在指向 obj = null 语句之前,Object对象有两条引用路径,其中一条为obj强引用类型,另一条为wr弱引用类型。此时无论如何也不会进行垃圾回收。当执行了obj = null.Object 对象就只具有弱引用,并且我们进行了显示的垃圾回收。因此此具有弱引用的对象就被GC给回收了。

4) PhantomReference

PhantomReference,即虚引用,虚引用并不会影响对象的生命周期。虚引用的作用为:跟踪垃圾回收器收集对象这一活动的情况。

当GC一旦发现了虚引用对象,则会将PhantomReference对象插入ReferenceQueue队列,而此时PhantomReference对象并没有被垃圾回收器回收,而是要等到ReferenceQueue被你真正的处理后才会被回收。

注意:PhantomReference必须要和ReferenceQueue联合使用,SoftReference和WeakReference可以选择和ReferenceQueue联合使用也可以不选择,这使他们的区别之一。

接下来看一个虚引用的例子。

 1public class TestPhantomReference { 2 3        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>(); 4        public static void main(String[] args){ 5 6            Object obj = new Object(); 7            PhantomReference<Object> pr = new PhantomReference<Object>(obj, rq); 8            System.out.println(pr.get()); 9            obj = null;10            System.gc();11            System.out.println(pr.get());12            Reference<Object> r = (Reference<Object>)rq.poll();13            if(r!=null){14                System.out.println("回收");15            }16        }17    }

运行结果:null null 回收

根据上面的例子有两点需要说明:

  • PhantomReference的get方法无论在上面情况下都是返回null。这个在PhantomReference源码中可以看到。

  • 在上面的代码中,如果obj被置为null,当GC发现虚引用,GC会将把 PhantomReference 对象pr加入到队列ReferenceQueue中,注意此时pr所指向的对象并没有被回收,在我们现实的调用了 rq.poll() 返回 Reference 对象之后当GC第二次发现虚引用,而此时 JVM 将虚引用pr插入到队列 rq 会插入失败,此时 GC 才会对虚引用对象进行回收。


总结

Refrence 和引用队列 ReferenceQueue 联合使用时,如果 Refrence持有的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。


最后的最后

卖一下广告,欢迎大家关注我的微信公众号,扫一扫下方二维码或搜索微信号 stormjun,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。