一提到Reference 百分之九十九的java程序员都懵逼了
阅读原文时间:2021年04月20日阅读:2

原来的标题是:"一提到Reference 99.99%的java程序员都懵逼了",为啥改成汉字了呢?吐槽一下,因为CSDN出bug了,如果你用了%做标题,你的文章就别想用它的编辑器修改了,它的js脚本写的不够健壮,报错了;

java.lang.ref.Reference

java程序员都知道如果一个对象没有任何引用了,那么这个对象在gc的时候就被回收了,大部分java程序员是基于我们常见的强引用去理解引用的,强引用这种方式虽然简单直接,但是对于一些特殊的java对象,如缓存数据在内存紧张时自动释放掉空间防止oom、直接内存对象回收之前需自动释放掉其占用的堆外内存,当socket对象被回收之前关闭连接,当文件流对象被回收之前自动关闭打开的文件等操作,强引用就无能为力了,为了实现这些特殊需求,java还引入了除了强引以外的引用类型来辅助对象回收操作 或 监控对象的生存周期。

先来看看引用类型的大家族:

在java.lang.ref包下除了ReferenceQueue类,Finalizer、FinalReference、PhantomReference、SoftReference、WeakReference都直接或间接继承了Reference类,想想为什么没有强引用StrongReference?

理解Reference的关键点在于:

1. 两个队列pending与ReferenceQueue的作用;

2. Reference对象4种状态的转换;

3. ReferenceHander线程的处理过程,它是如何将Pending与ReferenceQueue关联起来的;

4. Pending队列数据来源;

理解了以上4个问题,才算真不再对Reference懵逼了,下面就带着这四个问题去看下面的内容。

Reference构造方法

我们先来看看这些引用类型的父类都给我们提供了什么?

其内部提供2个构造函数,除了传入referent对象外,区别就是要不要传入一个ReferenceQueue队列

Reference(Treferent) {

this(referent,null);

}

Reference(T referent,ReferenceQueuesuper T>queue){

this.referent =referent;

this.queue = (queue==null)? ReferenceQueue.NULL:queue;

}

ReferenceQueue队列的作用就是Reference引用的对象被回收时,Reference对象能否进入到pending队列,最终由ReferenceHander线程处理后,Reference就被放到了这个队列里面(Cleaner对象除外,后面有一篇专门讲Cleaner对象源码),然后我们就可以在这个ReferenceQueue里拿到reference,执行我们自己的操作(至于什么操作就看你想怎么用了),所以这个队列起到一个对象被回收时通知的作用;

如果不带ReferenceQueue的话,要想知道Reference持有的对象是否被回收,就只有不断地轮训reference对象,通过判断里面的get是否为null(phantomReference对象不能这样做,其get始终返回null,因此它只有带queue的构造函数).这两种方法均有相应的使用场景,取决于实际的应用.如weakHashMap中就选择去查询queue的数据,来判定是否有对象将被回收.而ThreadLocalMap,则采用判断get()是否为null来作处理;

对于带ReferenceQueue参数的构造方法,如果传入的队列为null,那么就会给成员变量queue赋值为ReferenceQueue.NULL队列,这个NULL是ReferenceQueue对象的一个继承了ReferenceQueue的内部类,它重写了入队方法enqueue,这个方法只有一个操作,直接返回 false,也就是这个对列不会存取任何数据,它起到状态标识的作用;

Reference的重要成员变量

//在构造方法传入java对象最后就赋值给了referent,也就是它引用到的对象

private Treferent; /* Treated specially by GC */

  //pending队列, pending成员变量与后面的discovered对象一起构成了一个pending单向链表,注意这个成员变量是一个静态对象,所以是全局唯一的,pending为链表的头节点,discovered为链表当前Reference节点指向下一个节点的引用,这个队列是由jvm的垃圾回收器构建的,当对象除了被reference引用之外没有其它强引用了,jvm的垃圾回收器就会将指向需要回收的对象的Reference都放入到这个队列里面(好好理解一下这句话,注意是指向要回收的对象的Reference,要回收的对象就是Reference的成员变量refernt持有的对象,是refernt持有的对象要被回收,而不是Reference对象本身),这个队列会由ReferenceHander线程来处理(ReferenceHander线程是jvm的一个内部线程,它也是Reference的一个内部类,它的任务就是将pending队列中要被回收的Reference对象移除出来,如果Reference对象在初始化的时候传入了ReferenceQueue队列,那么就把从pending队列里面移除的Reference放到它自己的ReferenceQueue队列里(为什么是它自己的?pending队列是全局唯一的队列,但是Reference的queue却不是,它是在构造方法里面指定的,前面说过这里Cleaner对象是个特例),如果没有ReferenceQueue队列,那么其关联的对象就不会进入到Pending队列中,会直接被回收掉,除此之外ReferenceHander线程还会做一些其它操作,后面会讲到

/* List of References waiting to beenqueued.  The collector adds

* References to this list, while theReference-handler thread removes

* them. This list is protected by the above lock object. The

* list uses the discovered field to linkits elements.

*/

privatestatic Referencepending =null;

  //与成员变量pending一起组成pending队列,指向链表当前节点的下一个节点

/* When active:   next element in a discovered reference listmaintained by GC (or this if last)

*    pending:   next element in thepending list (or null if last)

*  otherwise:   NULL

*/

transientprivate Referencediscovered; /* used by VM */

//ReferenceQueue队列,这就是我们前面提到的起到通知作用的ReferenceQueue,需要注意的是ReferenceQueue并不是一个链表数据结构,它只持有这个链表的表头对象header,这个链表是由Refence对象里面的next成员变量构建起来的,next也就是链表当前节点的下一个节点(只有next的引用,它是单向链表),所以Reference对象本身就是一个链表的节点,这个链表数据来源前面讲pending队列的时候已经提到了,它是由ReferenceHander线程从pending队列中取的数据构建的(需要注意的是,这个对象并不是一个全局的,它是在构造方法里面传入进来的,所以Reference对象需要进入那个队列是我们自己指定的,也有特例,如FinalReference,Cleaner就是一个内部全局唯一的,无法指定,后面有两篇专门讲他们俩的源码),一旦Reference对象放入了队列里面,那么queue就会被设置为ReferenceQueue.ENQUEUED,来标识当前Reference已经进入到队里里面了;

volatileReferenceQueuesuper T>queue;

  //用来与queue成员变量一同组成ReferenceQueue队列,见上面queue的说明;

/* When active:   NULL

*     pending:  this

*   Enqueued:   next reference inqueue (or this if last)

*   Inactive:   this

*/

@SuppressWarnings("rawtypes")

Reference next;

    //lock成员变量是pending队列的全局锁,如果你搜索这个lock变量会发现它只在ReferenceHander线程run方法里面用到了,不要忘了jvm垃圾回收器线程也会操作pending队列,往pending里面添加Reference对象,所以需要加锁;

/* Object used to synchronize with thegarbage collector.  The collector

* must acquire this lock at the beginningof each collection cycle.  It is

* therefore critical that any code holdingthis lock complete as quickly

* as possible, allocate no new objects,and avoid calling user code.

*/

staticprivateclass Lock { };

privatestatic Lock_lock_ =new Lock();

这里一定要理解pending队列,什么样的Reference对象会进入这个队列。

进入这个队列的Reference对象需要满足两个条件:

  1. Reference所引用的对象已经不存在其它强引用;

  2. Reference对象在创建的时候,指定了ReferenceQueue;

Reference状态及其转换

如果去查看Reference源码,会发现这个类的开头有一段很长的注释,说明了Reference对象的四种状态:

/* A Reference instance is in one of four possible internal states:
     *
     *     Active: Subject to special treatment by the garbage collector.  Some
     *     time after the collector detects that the reachability of the
     *     referent has changed to the appropriate state, it changes the
     *     instance's state to either Pending or Inactive, depending upon
     *     whether or not the instance was registered with a queue when it was
     *     created.  In the former case it also adds the instance to the
     *     pending-Reference list.  Newly-created instances are Active.
     *
     *     Pending: An element of the pending-Reference list, waiting to be
     *     enqueued by the Reference-handler thread.  Unregistered instances
     *     are never in this state.
     *
     *     Enqueued: An element of the queue with which the instance was
     *     registered when it was created.  When an instance is removed from
     *     its ReferenceQueue, it is made Inactive.  Unregistered instances are
     *     never in this state.
     *
     *     Inactive: Nothing more to do.  Once an instance becomes Inactive its
     *     state will never change again.
     *
     * The state is encoded in the queue and next fields as follows:
     *
     *     Active: queue = ReferenceQueue with which instance is registered, or
     *     ReferenceQueue.NULL if it was not registered with a queue; next =
     *     null.
     *
     *     Pending: queue = ReferenceQueue with which instance is registered;
     *     next = this
     *
     *     Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance
     *     in queue, or this if at end of list.
     *
     *     Inactive: queue = ReferenceQueue.NULL; next = this.
     *
     * With this scheme the collector need only examine the next field in order
     * to determine whether a Reference instance requires special treatment: If
     * the next field is null then the instance is active; if it is non-null,
     * then the collector should treat the instance normally.
     *
     * To ensure that a concurrent collector can discover active Reference
     * objects without interfering with application threads that may apply
     * the enqueue() method to those objects, collectors should link
     * discovered objects through the discovered field. The discovered
     * field is also used for linking Reference objects in the pending list.
     */

如果你没有耐心看完这段注释,请直接往后看:

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

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

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

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

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

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

Reference对象的状态只需要通过成员变量next和queue来判断:

  1. Active:   next=null

  2. Pending:  next = this ,queue = ReferenceQueue

  3. Enqueued:  queue =ReferenceQueue.ENQUEUED

  4. Inactive:    next = this  ,queue = ReferenceQueue.NULL;

以下是Reference对象状态转换图,用word画的,将就着看吧:

特例还是要拿出来单独说,上面的图不适用描述Cleaner对象,Cleaner对象是没有Enqueue状态的,它经过HandReference处理时执行其clean方法清理,然后就直接进入了inactive状态了;

下面简单看一下他们的源码实现,如果你理解了上面所有内容,下面的源码理解起来非常简单,我只做简单的注释说明:

ReferenceHandler线程源码解析

ReferenceHandler线程是一个拥有最高优先级的守护线程,它是Reference类的一个内部类,在Reference类加载执行cinit的时候被初始化并启动;它的任务就是当pending队列不为空的时候,循环将pending队列里面的头部的Reference移除出来,如果这个对象是个Cleaner实例,那么就直接执行它的clean方法来执行清理工作;否则放入到它自己的ReferenceQueue里面;

所以这个线程是pending队列与ReferenceQueue的桥梁;

/* High-priority thread to enqueue pending References*/

private static class ReferenceHandler extends Thread {

ReferenceHandler(ThreadGroup g, String name) {

super(g, name);

}

public void run() {

for (;;) {

//从pending中移除的Reference对象

Reference r;

//此处需要加全局锁,因为除了当前线程,gc线程也会操作pending队列

synchronized (lock) {

//如果pending队列不为空,则将第一个Reference对象取出

if (pending != null) {

//缓存pending队列头节点

r = pending;

//将头节点指向discovered,discovered为pending队列中当前节点的下一个节点,这样就把第一个头结点出队了

pending = r.discovered;

//将当前节点的discovered设置为null;当前节点出队,不需要组成链表了;

r.discovered = null;

} else {//如果pending队列为空,则等待

try {

try {

lock.wait();

} catch(OutOfMemoryError x) { }

} catch(InterruptedException x) { }

continue;

}

}

// 如果从pending队列出队的r是一个Cleaner对象,那么直接执行其clean()方法执行清理操作;

if (r instanceof Cleaner) {

((Cleaner)r).clean();

//注意这里,这里已经不往下执行了,所以Cleaner对象是不会进入到队列里面的,给它设置ReferenceQueue的作用是为了让它能进入Pending队列后被ReferenceHander线程处理;

continue;

}

//将对象放入到它自己的ReferenceQueue队列里

ReferenceQueue q = r.queue;

if (q != ReferenceQueue.NULL) q.enqueue(r);

}

}

}

//以下是ReferenceHander线程初始化并启动的操作

static {

ThreadGroup tg =Thread.currentThread().getThreadGroup();

for (ThreadGroup tgn = tg;

tgn != null;

tg = tgn, tgn = tg.getParent());

//线程名称为Reference Handler

Thread handler = newReferenceHandler(tg, "Reference Handler");

/* If there were a special system-onlypriority greater than

* MAX_PRIORITY, it would be used here

*/

//线程有最高优先级

handler.setPriority(Thread.MAX_PRIORITY);

//设置线程为守护线程;

handler.setDaemon(true);

handler.start();

}

ReferenceQueue源码解析

ReferenceQueue队列是一个单向链表,ReferenceQueue里面只有一个header成员变量持有队列的队头,Reference对象是从队头做出队入队操作,所以它是一个后进先出的队列

public class ReferenceQueue {

public ReferenceQueue() { }

//内部类,它是用来做状态识别的,重写了enqueue入队方法,永远返回false,所以它不会存储任何数据,见后面的NULL和ENQUEUED两个标识成员变量

private static class Null extendsReferenceQueue {

boolean enqueue(Reference r) {

return false;

}

}

//当Reference对象创建时没有指定queue或Reference对象已经处于inactive状态

staticReferenceQueue NULL = new Null<>();

//当Reference已经被ReferenceHander线程从pending队列移到queue里面时

static ReferenceQueue ENQUEUED = new Null<>();

staticprivate class Lock { };

//出队入队时对队列加锁

privateLock lock = new Lock();

//队列头

privatevolatile Reference head = null;

//队列长度

private long queueLength = 0;

//入队操作,ReferenceHander调用此方法将Reference放入到队列里

boolean enqueue(Reference r) { /* Called only byReference class */

//加锁操作队列

synchronized(lock) {

//如果Reference创建时没有指定队列或Reference对象已经在队列里面了,则直接返回

ReferenceQueue queue = r.queue;

if ((queue == NULL) || (queue == ENQUEUED)) {

return false;

}

//只有r的队列是当前队列才允许入队

assert queue == this;

//将r的queue设置为ENQUEUED状态,标识Reference已经入队

r.queue = ENQUEUED;

//从队列头部入队

r.next = (head == null) ? r : head;

head = r;

//队列里面对象数量+1

queueLength++;

//如果r是一个FinalReference实例,那么将FinalReference数量也+1

if (r instanceof FinalReference) {

sun.misc.VM.addFinalRefCount(1);

/**Vm.addFinalRefCount(int n)方法的源码

publicstatic void addFinalRefCount(int n) {

// The caller must hold lock to synchronize the update.

finalRefCount += n;

if (finalRefCount > peakFinalRefCount) {

peakFinalRefCount = finalRefCount;

}

}

可以通过sun.misc.VM.getFinalRefCount()和sun.misc.VM.getPeakFinalRefCount()来获取FinalReference对象的当前数量和峰值数量

***/

}

//唤醒出队操作的等待线程

lock.notifyAll();

return true;

}

}

//Reference对象出队操作,将头部第一个对象移出队列,并将队列长度-1

@SuppressWarnings("unchecked")

private Reference reallyPoll() {       /* Must hold lock */

Reference r = head;

if (r != null) {

head = (r.next == r) ?

null :

r.next; // Unchecked due to the next field having a raw type inReference

r.queue = NULL;

r.next = r;

queueLength--;

if (r instanceof FinalReference) {

sun.misc.VM.addFinalRefCount(-1);

}

return r;

}

return null;

}

//将队列头部第一个对象从队列中移除出来,如果队列为空则直接返回null(此方法不会被阻塞)

public Reference poll() {

if (head == null)

return null;

synchronized (lock) {

return reallyPoll();

}

}

//将头部第一个对象移出队列并返回,如果队列为空,则等待timeout时间后,返回null,这个方法会阻塞线程

public Reference remove(long timeout)

throws IllegalArgumentException, InterruptedException

{

if (timeout < 0) {

throw new IllegalArgumentException("Negative timeout value");

}

synchronized (lock) {

Reference r = reallyPoll();

if (r != null) return r;

for (;;) {

lock.wait(timeout);

r = reallyPoll();

if (r != null) return r;

if (timeout != 0) return null;

}

}

}

public Reference remove() throws InterruptedException{

return remove(0);

}

}