由Reference展开的学习

在阅读Thinking in Java的Containers in depth一章中的Holding references时,提到了一个工具包java.lang.ref,说这是个为Java垃圾回收提供了很大的灵活性的包。

并引出了抽象类Reference还有它的三个子类,书上看了好几次都一脸懵逼……最后百度了很久现在简单记录总结一下。

一、为什么要有这个Reference类呢?

一般的对象在程序中,都是可获取的,也就是有直接引用的,或者用更专业的词(等等会介绍)就是强引用的对象。一般对于这样可以获得的对象,JVM的垃圾回收器Garbage Collection(简称gc)是不会回收它的内存的。但当一个对象是不可获得的,或者说没有指向它的引用的时候,gc会回收这个对象的内存。

但如果有些这样的情况,我们想继续持有对这个对象的引用,希望以后能继续访问这个对象,但同时又希望gc能在内存紧缺或正确的时候回收它的内存。这个时候就轮到我们的Reference类登场了。

这个类可以看作是一个普通的引用和你的对象之间的一个代理。注意,如果你想用Reference达到上面说的效果,这个被代理引用的对象,除了现在这个Reference对象包装的引用,不能有其他的普通引用(其他强引用),否则JVM发现后不会在正确的时候对它进行回收的。

二、Reference类

Java中的引用有四种,这里先简单提一下:强引用,软引用,弱引用还有虚引用。

强引用在Java中没有对应的类,其他三个引用类型都是Reference类的子类,分别是SoftReference、WeakReference、PhantomReference。

等等还会针对这四个引用进行详细地介绍。

然后我们来看看Reference类里面重要的field:

private T referent;       
volatile ReferenceQueue<? super T> queue;  
  
/* When active:   NULL 
 *     pending:   this 
 *    Enqueued:   next reference in queue (or this if last) 
 *    Inactive:   this 
 */  
@SuppressWarnings("rawtypes")  
Reference next;  
  

transient private Reference<T> discovered;  /* used by VM */  
  

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

referent:

既然要充当某个引用的代理,肯定要包着这个实际对象的引用吧,这个referent就是这个实际对象的引用。

static Reference<Object>和Reference<T>discovered:

这两个field组成了个pending单向链表。pending是static field,所以是Reference类中唯一的一个。Pending为链表的头节点,

discovered为当前的Pending链表中我这个Reference节点的下一个节点的引用。

注意!!这个链表呢是JVM的gc构建的!!  JVM大概是这样的,就gc在准备回收的时候,如果发现了普通引用的代理也就是Reference类的实例有以下特点:

  1. Reference所引用或者说所代理的实际引用指向的那个实际对象,这个实际对象除了这个Reference引用外,不存在其他强引用(普通引用)了。

  2. 这个Reference对象,在创建的时候,构造器中指定了ReferenceQueue。

那么JVM就会把这整个Reference instance放进这个pending链表中。

哎如果不满足条件2,也就是说Reference instance没有指定ReferenceQueue的话,那么这个Reference对象是不会进入Pending链表的,会被直接回收掉。

总之这个Pending和discovered是由JVM进行赋值。

然后这个Pending链表呢,主要是由一个叫做ReferenceHander的线程来处理的,这个线程是JVM的一个内部线程,也是Reference的一个内部嵌套类(static类),它的任务捏,就是把Pending上准备回收内存的实际对象的代理引用Reference instance拿出来,然后放到它自己的private ReferenceQueue中去。

ReferenceQueue queue和next:

这两个组成了一个队列——ReferenceQueue

ReferenceQueue并不是一个链表数据结构,它只持有这个链表的表头对象header,这个链表是由Refence对象里面的next成员变量构建起来的,next也就是链表当前节点的下一个节点(只有next的引用,它是单向链表),所以Reference对象本身就是一个链表的节点。

那么这个队列是干嘛用的呢?

可以看作是实际对象被回收时,作为通知用的。    从刚刚对pending链表的介绍我们知道,ReferenceQueue的数据也就是Reference instance是从Pending链表中由ReferenceHander线程搞过来的。所以通过这个ReferenceQueue队列,你就可以知道哪些代理引用所指的实际对象要被回收了,然后你可以从这个ReferenceQueue中通过Reference拿到要回收的实际对象,然后做些操作。

这里还要介绍两个ReferenceQueue类中的static field:

//当Reference对象创建时没有指定queue或Reference对象已经处于inactive状态
staticReferenceQueue<Object> NULL = new Null<>();

//当Reference已经被ReferenceHander线程从pending队列移到queue里面时
static ReferenceQueue<Object> ENQUEUED = new Null<>();

 第一个NULL是一个Null对象,Null不是我们的平常用的null,而是一个ReferenceQueue对象的一个继承了ReferenceQueue的内部类,它重写了入队方法enqueue,这个方法只有一个操作,直接返回 false,也就是这个对列不会存取任何数据,它起到状态标识的作用。

当你构造Reference instance的时候,如果构造器中没有传入ReferenceQueue,或者传入的这个队列为null,那么这个Reference的instance的成员变量queue就会被赋值为ReferenceQueue.NULL。

第二个ENQUEUED也是类似的道理,是个状态标识的变量。一旦这个Reference instance被从Pending链表中拿出来放进它的ReferenceQueue队列中后,这个Reference instance的成员变量queue就会被赋值成ReferenceQueue.ENQUEUED。

Reference的状态及其转换

Reference一共有四个状态:

1.       Active:活动状态,对象存在强引用状态,还没有被回收;


2.       Pending:垃圾回收器将没有强引用的Reference对象放入到pending队列中,等待ReferenceHander线程处理(前提是这个Reference对象创建的时候传入了ReferenceQueue,否则的话对象会直接进入Inactive状态)。也就是马上要回收的对象;


3.       Enqueued:ReferenceHander线程将pending队列中的对象取出来放到ReferenceQueue队列里;

当引用实例被添加到它注册在的引用队列中时,该实例处于Enqueued状态。当某个引用实例被从引用队列中删除后,该实例将从Enqueued状态变为Inactive状态。如果某个引用实例没有注册在一个引用队列中,该实例将永远不会进入Enqueued状态。

4.       Inactive:处于此状态的Reference对象可以被回收,并且其内部封装的对象也可以被回收掉了,
有两个路径可以进入此状态,

  路径一:在创建时没有传入ReferenceQueue的Reference对象,被Reference封装的对象在没有强引用时,指向它的Reference对象会直接进入此状态;

  路径二、此Reference对象经过前面三个状态后,已经由外部从ReferenceQueue中获取到,并且已经处理掉了。

上个状态转换图:

事实上Reference类并没有显示地定义内部状态值,JVM仅需要通过成员queue和next的值就可以判断当前引用实例处于哪个状态:

  • Active:queue为创建引用实例时传入的ReferenceQueue的实例或是ReferenceQueue.NULL;next为null
  • Pending:queue为创建引用实例时传入的ReferenceQueue的实例;next为this(这里其实想了很久不太知道为什么,这个状态不是还没把Reference instance搞到ReferenceQueue里面吗??不理了hh)
  • Enqueued:queue为ReferenceQueue.ENQUEUED;next为队列中下一个需要被处理的实例或是this如果该实例为队列中的最后一个
  • Inactive:queue为ReferenceQueue.NULL;next为this

一个检测一个对象是否被回收的例子:

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

  • 创建一个引用队列 queue
  • 创建 Refrence 对象,并关联引用队列 queue
  • 在 reference 被回收的时候,refrence 会被添加到 queue 中
//创建一个引用队列  

ReferenceQueue queue = new ReferenceQueue();  
  


// 创建弱引用,此时状态为Active,并且Reference.pending为空,当前Reference.queue = 上面创建的queue,并且next=null  

WeakReference reference = new WeakReference(new Object(), queue);  

System.out.println(reference);  


// 当GC执行后,由于是弱引用,所以回收该object对象,并且置于pending上,此时reference的状态为PENDING  

System.gc();  
  



/* ReferenceHandler从pending中取下该元素,并且将该元素放入到queue中,此时Reference状态为ENQUEUED,Reference.queue = ReferenceENQUEUED */  
 



 
/* 当从queue里面取出该元素,则变为INACTIVE,Reference.queue = Reference.NULL */  

Reference reference1 = queue.remove();  
System.out.println(reference1);

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

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

  1. 监听 Activity 的生命周期
  2. 在 onDestroy 的时候,创建相应的 Refrence 和 RefrenceQueue,并启动后台进程去检测
  3. 一段时间之后,从 RefrenceQueue 读取,若读取不到相应 activity 的 Refrence,有可能发生泄露了,这个时候,再促发 gc,一段时间之后,再去读取,若在从 RefrenceQueue 还是读取不到相应 activity 的 refrence,可以断定是发生内存泄露了
  4. 发生内瘘泄露之后,dump,分析 hprof 文件,找到泄露路径

关于Java的几种引用类型

强引用:

叫StrongReference,

这个引用在Java中没有相应的类与之对应,但是强引用比较普遍Object obj = new Object(); obj就为一个强引用,obj=null后, 该对象可能会被JVM回收。

如果一个对象具有强引用,则垃圾回收器始终不会回收此对象。当内存不足时,JVM情愿抛出OOM异常使程序异常终止也不会靠回收强引用的对象来解决内存不足的问题。

软引用:

SoftReference类

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

看个例子:

public class TestSoftReference {
        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
        public static void main(String[] args){
            Object obj = new Object();
            SoftReference<Object> sf = new SoftReference(obj,rq);
            System.out.println(sf.get()!=null);
            System.gc();
            obj = null;  //这里就是要让这个对象除了软引用中封装的强引用外,没有别的强引用了
            System.out.println(sf.get()!=null);

        }
    }

运行结果均为:true。

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

弱引用:

WeakReference类

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

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

看一个例子:

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

    }

运行结果为: true 、false

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

虚引用

PhantomReference类

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

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

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

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

public class TestPhantomReference {

        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
        public static void main(String[] args){

            Object obj = new Object();
            PhantomReference<Object> pr = new PhantomReference<Object>(obj, rq);
            System.out.println(pr.get());
            obj = null;
            System.gc();
            System.out.println(pr.get());
            Reference<Object> r = (Reference<Object>)rq.poll();
            if(r!=null){
                System.out.println("回收");
            }
        }
    }

运行结果:null null 回收

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

  • PhantomReference的get方法无论在上面情况下都是返回null。这个在PhantomReference源码中可以看到。
  • 在上面的代码中,如果obj被置为null,当GC发现虚引用,GC会将把 PhantomReference 对象pr加入到队列ReferenceQueue中,注意此时pr所指向的对象并没有被回收,在我们现实的调用了 rq.poll() 返回 Reference 对象之后当GC第二次发现虚引用,而此时 JVM 将虚引用pr插入到队列 rq 会插入失败,此时 GC 才会对虚引用对象进行回收。(这一点意思大概就是,其实也和别的引用一样嘛,在referenceQueue中的虚引用被处理后,比如像这样被poll出来后,才会被回收。  这里描述的应该是源码的逻辑吧)

 参考文章:

  《一提到Reference 百分之九十九的java程序员都懵逼了》——https://blog.csdn.net/zqz_zqz/article/details/79474314

  《java 源码系列 - 带你读懂 Reference 和 ReferenceQueue》——https://blog.csdn.net/gdutxiaoxu/article/details/80738581

  《java中的Reference 》——https://www.cnblogs.com/zyzl/p/5540248.html

  《Java源码剖析——彻底搞懂Reference和ReferenceQueue》——http://www.wxueyuan.com/blog/articles/2017/12/01/1512114060269.html

  

猜你喜欢

转载自www.cnblogs.com/wangshen31/p/10363556.html