美文网首页Android
java源码:Reference和ReferenceQueue

java源码:Reference和ReferenceQueue

作者: huangLearn | 来源:发表于2020-08-22 15:51 被阅读0次

    我们都知道在堆里面存放着Java中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。那么gc怎么判断一个对象是不是垃圾呢

    判断对象是否存活有两种计数算法:引用计数法、可达性分析法

    引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一 就是如果一个对象没有被任何引用指向,则可视之为垃圾。

    可达性分析法:通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链Reference Chain,当一个对象到GC Root没有任何引用链相连时,则证明此对象是不可用的

    这两种方式我们不做过多的介绍。但我们可以发现无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和引用离不开关系。

    下来我们就开始说说引用

    引用

    要了解ReferenceReferenceQueue,我们需要先知道什么是引用。

    我们用图来展示一下 Javanew一个对象 在内存中的创建过程

    image.png

    我们可以看出创建一个对象并用一个引用指向它的过程:

    1.在堆内存中创建一个Student类的对象(new Student()

    2.在栈内存中声明一个指向Student类型对象的变量(Student obj

    3.将栈内存中的引用变量obj指向堆内存中的对象new Student()

    这样把一个对象赋给一个变量,这个变量obj就是引用

    JDK1.2版之前,Java里面的引用是很传统的定义:
    如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。

    JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为4种:
    强引用(Strong Reference
    软引用(Soft Reference
    弱引用(Weak Reference
    虚引用(Phantom Reference

    Java 中引入四种引用的目的就是让程序自己决定对象的生命周期。JVM通过垃圾回收器对这四种引用做不同的处理,来实现对象生命周期的改变。接下来就来看一下四种引用

    强引用

    java中最常用的引用类型,如Object obj = new Object() ,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。在 java.lang.ref 中并没有实际的对应类型。

    强引用指向的对象任何时候都不会被回收,垃圾回收器宁愿抛出OOM也不会对该对象进行回收

    例子:

     Object o = new Object();
     System.out.println("before gc is "+ o);//java.lang.Object@2328c243
     System.gc();
     System.out.println("after gc is "+ o);//java.lang.Object@2328c243
     try {
         //手动调节堆内存,让程序OOM异常,看强引用回收情况  -Xms10m -Xmx10m
         byte[] bytes = new byte[11 * 1024 * 1024];
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         System.out.println("after oom is "+ o);//java.lang.Object@2328c243
     }
    

    软引用

    软引用是一种相对强引用弱化了一些的引用,对应的类为java.lang.ref.SoftReference

    如果一个对象仅持有软引用,内存空间足够,垃圾回收器就不会回收它;但是如果内存空间不足 ,才回去回收软引用中的对象.

    例子:

     SoftReference<Object> softReference = new SoftReference<>( new Object());
     System.out.println("before gc is "+ softReference.get());//java.lang.Object@2328c243
     System.gc();
     System.out.println("after gc is "+ softReference.get());//java.lang.Object@2328c243
     try {
           //手动调节堆内存,让程序OOM异常,看软引用回收情况  -Xms10m -Xmx10m
           byte[] bytes = new byte[11 * 1024 * 1024];
     } catch (Exception e) {
           e.printStackTrace();
     } finally {
          System.out.println("after oo   m is "+ softReference.get());//null
     }
    

    我们可以看到软引用和强引用不同的是是内存够用的时候就保留,不够用就回收。

    弱引用

    弱引用对应ava.lang.ref.WeakReference类,它比软引用的生存期更短,如果一个对象仅持有弱引用,当发生垃圾回收时,不管当前内存是否足够,都会将弱引用关联的对象进行回收。

    例子:

     WeakReference weakRef = new WeakReference<Object>(new Object());
     System.out.println("gc之前的值:" + weakRef.get()); // java.lang.Object@2328c243
     System.gc();
     System.out.println("gc之后的值:" + weakRef.get());//null
    

    虚引用

    虚引用,顾名思义,就是形同虚设, 对应的类为java.lang.ref.PhantomReferenc 。与其他几种引用都不太一样,虚引用并不能决定对象的生命周期,也无法通过虚引用来取得一个对象实例。虚引用的主要作用是跟踪对象垃圾回收的状态。PhantomReferenceget()方法永远返回null,而且只提供了与引用队列同用的构造函数。所以虚引用必须和引用队列一同使用。

    例子:

    ReferenceQueue referenceQueue = new ReferenceQueue();
    PhantomReference<Object> phantomReference =  new PhantomReference<>(new Object(), referenceQueue);
    System.out.println("before gc phantomReference is "+ phantomReference.get());//null
    System.out.println("before gc referenceQueue is "+ referenceQueue.poll());//null
    System.gc();
    System.out.println("after gc phantomReference is "+ phantomReference.get());//null
    System.out.println("after gc referenceQueue is "+ referenceQueue.poll());//ava.lang.ref.PhantomReference@2328c243
    

    ReferenceQueue

    对于软引用、弱引用和虚引用,都可以和一个引用队列ReferenceQueue 联合使用,如果软/弱/虚引用中的对象被回收,那么软/弱/虚引用就会被 JVM加入关联的引用队列ReferenceQueue中。 也就是说我们可以通过监控引用队列来判断Reference引用的对象是否被回收,从而执行相应的方法。
    例如下面的例子. 如果弱引用中的对象(obj)被回收,那么软引用weakRef就会被 JVM 加入到引用队列queue 中。 这样当我们想检测obj对象是否被回收了 ,就可以从 queue中读取相应的 Reference 来判断obj是否被回收,从而执行相应的方法。

    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    Object obj = new Object();
    WeakReference weakRef = new WeakReference<Object>(obj,queue);
    obj = null;
    System.out.println("gc之后的值: " + weakRef.get()); // 对象依然存在
    //调用gc
    System.gc();
    //如果obj被回收,则软引用会进入引用队列
    Reference<?> reference = queue.remove();
    if (reference != null){
        System.out.println("对象已被回收: "+ reference.get());  // 对象为null
    }
    

    Reference源码(JDK8)

    要理解Reference 源码要从几个方面来看:
    1.Reference 对象的4种状态是如何转换
    2.pending-Reference list的赋值和作用
    3.Reference-handler的作用
    4.ReferenceQueue的作用

    我们打开Reference的源码,可以看到最开始有一段注释说明
    介绍了引用的四种状态Active ,Pending ,Enqueued ,Inactive

    image.png
    翻译过来大意如下:
    1)Active
    新创建的Reference实例的状态是ActiveGC检测到Reference引用的实际对象的可达性发生某些改变后,它的状态将变化为PendingInactive
    Reference注册了ReferenceQueue,则会切换为Pending,并且Reference会加入pending-Reference list中。
    Reference没有注册ReferenceQueue,会切换为Inactive
    2)Pending
    pending-Reference list中等待着被Reference-handler 入队列queue中的元素就处于这个状态。没有注册ReferenceQueue的实例是永远不可能到达这一状态。
    3) Enqueued
    Enqueued:在ReferenceQueue队列中时Reference的状态,如果Reference从队列中移除,会进入Inactive状态
    4)Inactive
    一旦一个实例变为Inactive,则这个状态永远都不会再被改变。
    它们的关系下图很清晰的表示了出来 image.png

    Reference源码中并不存在一个成员变量用于描述Reference的状态,它是通过他的成员变量的存在性"拼凑出"对应的状态。

    然后我们看下Reference内部的成员变量

    public abstract class Reference<T> {
          private T referent;    
          volatile ReferenceQueue<? super T> queue;
          Reference next;
          transient private Reference<T> discovered;
          static private class Lock { }
          private static Lock lock = new Lock();
          private static Reference<Object> pending = null;
    }
    

    referent:指reference引用的对象

    queue:引用队列,Reference引用的对象被回收时,Reference实例会被放入引用队列,我们可以从ReferenceQueue得到Reference实例,执行我们自己的操作

    next:下一个Reference实例的引用,Reference实例通过此构造单向的链表。 ReferenceQueue并不是一个链表数据结构,它只持有这个链表的表头对象header,这个链表就是由next构建起来的,next也就是链表当前节点的下一个节点

    pending:等待加入队列的引用列表,GC检测到某个引用实例指向的实际对象不可达后,会将该pending指向该引用实例。pendingdiscovered一起构成了一个pending单向链表,pending为链表的头节点,discovered为链表当前Reference节点指向下一个节点的引用,这个队列是由jvm的垃圾回收器构建的,当对象除了被reference引用之外没有其它强引用了,jvm的垃圾回收器就会将指向需要回收的对象的Reference都放入到这个队列里面。这个队列会由ReferenceHander线程来处理,它的任务就是将pending队列中要被回收的Reference对象移除出来,

    discovered:pending list中下一个需要被处理的实例,在处理完当前pending之后,将discovered指向的实例赋予给pending即可。所以这个pending就相当于是一个链表。

    我们来看一个弱引用的回收过程,来了解他的成员变量和四种状态的转换

     ReferenceQueue<Object> queue = new ReferenceQueue<>();
     WeakReference mWreference = new WeakReference(new Object(), queue); 
     System.gc();
     Reference mReference = queue.remove();
    

    1.创建弱引用,此时状态为Active,pending= null,discovered = null
    2.执行GC,由于是弱引用,所以回收该object对象,将引用mWreference 放入pending队列,等待被ReferenceHandler线程处理.此时状态为PENDING,pending=mWreference,discovered = pending-Reference列表中的下一个元素
    3.ReferenceHandlerpending队列中取下mWreference,并且将mWreference放入到queue中,此时Reference状态为 Enqueued,调用了ReferenceQueue.enqueued()后的Reference实例就会处于这个状态
    4.当从queue里面取出该元素,则变为INACTIVE

    ReferenceHandler

    从上面的分析我们知道ReferenceHandle线程的主要功能就是把pending list中的引用实例添加到引用队列ReferenceQueue中,并将pending指向下一个引用实例。

    ReferenceHandlerReference类的一个内部类,由Reference静态代码块中建立并且运行的线程,只要Reference这个父类被初始化,该线程就会创建和运行,由于它是守护线程,除非JVM进程终结,否则它会一直在后台运行 。

     private static class ReferenceHandler extends Thread {
           ...
            public void run() {
                while (true) {
                    tryHandlePending(true);
                }
            }
            ...
        }
    
       static boolean tryHandlePending(boolean waitForNotify) {
            ...
                synchronized (lock) {
                   //如果pending队列不为空,则将第一个Reference对象取出
                    if (pending != null) {
                        //缓存pending队列头节点
                        r = pending;
                        // 'instanceof' might throw OutOfMemoryError sometimes
                        // so do this before un-linking 'r' from the 'pending' chain...
                        c = r instanceof Cleaner ? (Cleaner) r : null;
                        // unlink 'r' from 'pending' chain
                        //将头节点指向discovered,discovered为pending队列中当前节点的下一个节点,这样就把第一个头结点出队了
                        pending = r.discovered;
                         //将当前节点的discovered设置为null;当前节点出队,不需要组成链表了;
                        r.discovered = null;
                    } else {
                        // The waiting on the lock may cause an OutOfMemoryError
                        // because it may try to allocate exception objects.
                        if (waitForNotify) {
                            lock.wait();
                        }
                        // retry if waited
                        return waitForNotify;
                    }
                }
                          //将对象放入到它自己的ReferenceQueue队列里
    
            ReferenceQueue<? super Object> q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
            return true;
        }
    
    

    ReferenceQueue

    ReferenceQueue:在Reference引用的对象被回收时,Reference对象进入到pending队列, 由ReferenceHander线程处理后,Reference就被放到ReferenceQueue里面,然后我们就可以从ReferenceQueue里拿到reference,执行我们自己的操作。这样我们只需要 ReferenceQueue就可以知道Reference持有的对象是否被回收。

    如果不带ReferenceQueue的话,要想知道Reference持有的对象是否被回收,就只有不断地轮训reference对象,通过判断里面的get是否为null(phantomReference对象不能这样做,其get始终返回null,因此它只有带queue的构造函数)。

    这两种方法均有相应的使用场景。如weakHashMap中就选择去查询queue的数据,来判定是否有对象将被回收.而ThreadLocalMap,则采用判断get()是否为null来作处理;

    ReferenceQueue只存储了Reference链表的头节点,真正的Reference链表的所有节点是存储在Reference实例本身,Reference通过成员属性next构建单向链表,ReferenceQueue提供了对Reference链表的入队、pollremove等操作

    public class ReferenceQueue<T> {
    
       boolean enqueue(Reference<? extends T> r) {  
           synchronized (lock) {
           
              // 如果引用实例持有的队列为ReferenceQueue.NULL或者ReferenceQueue.ENQUEUED则入队失败返回false
               ReferenceQueue<?> queue = r.queue;
               if ((queue == NULL) || (queue == ENQUEUED)) {
                   return false;
               }
    
               assert queue == this;
               // 当前引用实例已经入队,那么它本身持有的引用队列实例置为ReferenceQueue.ENQUEUED
               r.queue = ENQUEUED;
               // 接着,将 Reference 插入到链表
               // 如果链表没有元素,则此引用实例直接作为头节点,否则把前一个引用实例作为下一个节点
               r.next = (head == null) ? r : head;
               // 当前实例更新为头节点,也就是每一个新入队的引用实例都是作为头节点,已有的引用实例会作为后继节点
               head = r;
               // 队列长度增加1
               queueLength++;
               if (r instanceof FinalReference) {
                   sun.misc.VM.addFinalRefCount(1);
               }
               lock.notifyAll();
               return true;
           }
       }
    
       // 引用队列的poll操作,此方法必须在加锁情况下调用
       private Reference<? extends T> reallyPoll() {       /* Must hold lock */
           Reference<? extends T> r = head;
           if (r != null) {
               r.queue = NULL;
               // Update r.queue *before* removing from list, to avoid
               // race with concurrent enqueued checks and fast-path
               // poll().  Volatiles ensure ordering.
               @SuppressWarnings("unchecked")
               Reference<? extends T> rn = r.next;
               // Handle self-looped next as end of list designator.
               // 更新next节点为头节点,如果next节点为自身,说明已经走过一次出队,则返回null
               head = (rn == r) ? null : rn;
               // Self-loop next rather than setting to null, so if a
               // FinalReference it remains inactive.
               // 当前头节点变更为环状队列,考虑到FinalReference尚为inactive和避免重复出队的问题
               r.next = r;
               // 队列长度减少1
               queueLength--;
               // 特殊处理FinalReference,VM进行计数
               if (r instanceof FinalReference) {
                   VM.addFinalRefCount(-1);
               }
               return r;
           }
           return null;
       }
    
       // 队列的公有poll操作,主要是加锁后调用reallyPoll
       public Reference<? extends T> poll() {
           if (head == null)
               return null;
           synchronized (lock) {
               return reallyPoll();
           }
       }
    }
    
    

    相关文章

      网友评论

        本文标题:java源码:Reference和ReferenceQueue

        本文链接:https://www.haomeiwen.com/subject/jhsdjktx.html