java源码:Reference和ReferenceQueue

我们都知道在堆里面存放着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();
       }
   }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容