引用(1) - Reference和ReferenceQueue源码分析

Posted by zhidaliao on October 23, 2016

看文章前个人建议先把源码看一遍,带着疑问去阅读会好一点。我自己在看这个类的时候也有好多东西不明白,参考了几个大神的文章,重新整理了一遍。

Reference

所有引用对象的抽象基类,为所有的引用对象定义了通用的操作。因为引用对象的实现在与垃圾收集器存在紧密合作

先看 Reference 每个域的作用,主要有以下几个

private T referent;         /* Treated specially by GC */

ReferenceQueue<? super T> queue;

Reference next;

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

static private class Lock { };

private static Lock lock = new Lock();

private static Reference pending = null;

reference状态的表示方式

在看 reference注释的时候,标注了几个状态码,并做了解释。但是我们并没有在类中发现类似 state 的状态变量, 因为jvm并不需要定义状态值来判断相应引用的状态处于哪个状态,只需要通过计算next和queue即可进行判断。一个引用实例有四种内部状态:

Active:

被垃圾收集器特殊对待的对象,有时候在收集器检测它的可达性状态后会改变对象的状态,可以转换成 pending 和 Inactive 状态,主要取决于被创建的时候是否在 ReferenceQueue 队列中注册。 正常情况下他也会被添加到 pending-Reference 集合中。新创建的实例是 Active状态。

Pending:

未被注册的实例不存在这个状态。当垃圾回收器检测到可达性发生变化(变为不可达时),如果queue不为空,则加入到 Reference的静态变量pending的队列中,并将状态设置为 Pending。 元素在 pending-Reference 集合中,等待Reference-handler 线程将它入队列。

Enqueued:

当实例被创建,并且之后入队列了就是这个状态。当实例被移除 ReferenceQueue 队列, 他就是 Inactive 状态,没被注册的实例不存在这个状态

Inactive:

当垃圾回收器检测到可达性发生变化(变为不可达时),如果 queue == ReferenceQueue.Null的话,状态直接变为 InActive,实例进入这个状态不会再有任何改变。

JVM通过计算next和queue判断:
  • Active: next = null;

  • Pending: queue = ReferenceQueue 补充。。

  • Enqueue: 进入队列中的Reference 中的 next 为队列中一个引用,或等于this(表示当前引用为最后一个), queue = ReferenceQueue.ENQUEUE。

  • InActive: queue = ReferenceQueue.NULL; next = this

  • 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 = Following instance in queue, or this if at end of list. *
    • 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 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. */

T referent

referent代表的是引用的对象,比如 Object obj = new Object() , new Object(),为引用的对象描述当前引用所引用的实际对象,正如在注解中所述,其会认真对待.即什么时候会被回收,如果一旦被回收,则会直接置为null,而外部程序可通过通过引用对象本身(而不是referent)了解到,回收行为的产生.

referent表示其引用的对象,即我们在构造的时候,需要被包装在其中的对象.对象即将被回收的定义,即此对象除了被reference引用之外没有其它引用了(并非确实没有被引用,而是gcRoot可达性不可达,以避免循环引用的问题).

ReferenceQueue<? super T> queue

每个Reference对象都有一个对应的ReferenceQueue,用来存储Reference对象。一个ReferenceQueue对象可以被多个Reference共享。

“ Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected. ”

当被引用的对象被GC回收之后,GC会把Reference对象添加到队列中。因此,可以这样理解,在队列中的Reference对象,它们引用的对象被GC回收。而不在队列中的,它们引用的对象则还没有被GC回收。这个队列的存在,可以帮助开发者执行一些与被引用对象相关联的数据清理的动作。

如果在创建ReferenceQueue的时候没有指定队列,这个Reference就会使用默认的ReferenceQueue.Null队列,生命周期直接由 Active 进入 Inactive: queue即是对象即被回收时所要通知的队列,当对象即被回收时,整个reference对象会被放到queue里面,然后外部程序即可通过监控这个queue拿到相应的数据了.

ReferenceQueue类型的queue 是一个后入先出的队列,先看看他的源码和关键的几个方法:

全局域
static ReferenceQueue NULL = new Null();
static ReferenceQueue ENQUEUED = new Null();

当引用对象被移除队列的时候,对象的 queue 将会重置为空值的 ReferenceQueue

当引用对象入列的时候,对象的 queue 将会置为空值的ENQUEUED,标志这个对象已经被处理,避免重复操作。

enqueue()
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
    synchronized (r) {
        if (r.queue == ENQUEUED) return false;
        synchronized (lock) {
            r.queue = ENQUEUED;
            r.next = (head == null) ? r : head;
            head = r;
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll();
            return true;
        }
    }
}

这个队列并没有相对应的Node数据结构,其自己仅存储当前的head节点,队列的维护主要依靠 Reference的 next 节点来完成。enqueue 方法的示意图如下所示

image

reallyPoll()

将引用对象从队列中移除,后入先出,没什么好讲的

private Reference<? extends T> reallyPoll() {       /* Must hold lock */
    if (head != null) {
        Reference<? extends T> r = head;
        head = (r.next == r) ? null : r.next;
        r.queue = NULL;
        r.next = r;
        queueLength--;
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(-1);
        }
        return r;
    }
    return null;
}
poll()

轮询查看是否队列中有元素可用,如果有直接返回,否则返回null


public Reference<? extends T> poll() {
    if (head == null)
        return null;
    synchronized (lock) {
        return reallyPoll();
    }
}
remove

从队列中移除一个最近的引用对象,在规定时间内将一直阻塞直到有可用的元素返回

public Reference<? extends T> remove(long timeout)  throws IllegalArgumentException, InterruptedException
{
    if (timeout < 0) {
        throw new IllegalArgumentException("Negative timeout value");
    }
    synchronized (lock) {
        Reference<? extends T> r = reallyPoll();
        if (r != null) return r;
        for (;;) {
            lock.wait(timeout);
            r = reallyPoll();
            if (r != null) return r;
            if (timeout != 0) return null;
        }
    }
}

Reference next

在queue中 描述当前引用节点所存储的下一个节点.但next仅在放到queue中才会有意义.用于维护 queue 链表

为了描述相应的状态值,在放到队列当中后,其queue就不会再引用初始化时注册的引用队列了.而是引用一个特殊null值的ENQUEUED.因为已经放到队列当中,并且不会再次放到队列当中.

Lock lock = new Lock();

为了避免对变量同时进行操作,设置锁来确保并发异常。

Reference discovered;

使用关键字 transient 修饰,根据注释可以看到只要是给 VM 参考使用的。

表示要处理的对象的下一个对象.即可以理解要处理的对象也是一个链表,通过discovered进行排队

未完。。。

Reference pending

注意这个pending队列是一个静态类变量。可以理解为整个VM维护着一个pending队列。

此队列维护着需要进入引用队列的引用,由JVM虚拟机垃圾回收器在检测到被引用指向的对象可达性发生改变后,如果该对象的引用(Referecnce)注册了引用队列(ReferenceQueue),则JVM虚拟机垃圾收集器会将该引用加入到pending队列

同时,另一个字段discovered,表示要处理的对象的下一个对象.即可以理解要处理的对象也是一个链表,通过discovered进行排队,这边只需要不停地拿到pending,然后再通过discovered不断地拿到下一个对象即可.因为这个pending对象,两个线程都可能访问,因此需要加锁处理.

pending是由jvm来赋值的,当Reference内部的referent对象的可达状态改变时,jvm会将Reference对象放入pending链表。

pending & discovered

private static Reference pending = null;

//这个对象,定义为 private,并且全局没有任何给它赋值的地方,

再看discovered,同样为private,上下文也没有任何地方使用它

transient private Reference discovered; //看到了它的注释也明确写着是给VM用的。

上面两个变量对应在VM中的调用,可以参考openjdk中的hotspot源码,在hotspot/src/share/vm/memory/referenceProcessor.cpp 的ReferenceProcessor::discover_reference 方法。(根据此方法的注释由了解到虚拟机在对Reference的处理有ReferenceBasedDiscovery和RefeferentBasedDiscovery两种策略)

构造方法

/* -- Constructors -- */

Reference(T referent) {
    this(referent, null);
}

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

两个构造方法,一个没有引用队列 ReferenceQueue , 一个有ReferenceQueue,取决于你是否需要通知机制。

通知机制:每个引用可以关联一个引用队列,该引用队列由应用程序创建的,然后垃圾回收器在检测到引用不可达时,将该引用加入到该队列,应用程序可以根据该引用队列来做些处理。(也就是该引用队列 成为 垃圾回收器与应用程序的通信机制)

ReferenceQueue是作为 JVM GC与上层Reference对象管理之间的一个消息传递方式,它使得我们可以对所监听的对象引用可达发生变化时做一些处理,WeakHashMap正是利用此来实现的。

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

而如果不带的话,就只有不断地轮训reference对象,通过判断里面的get是否返回null(phantomReference对象不能这样作,其get始终返回null,因此它只有带queue的构造函数).

场景
SoftReference sf = new SoftReference( new Object() ); 

其中sf 为引用,new Object为 sf指向的对象,,其实也就是建立了 sf 到 new Object对象的引用(关联) 然后垃圾回收器发现new Object的可达性发生变化(其实就是变为不可达后), 此时JVM虚拟机会根据引用对象 sf 的 queue是否为空,如果为空,则直接将引用的状态变为 InActivie(非激活,离真正回收不远了)

ReferenceQueue queue = new ReferenceQueue();
SoftReference sf2 = new SoftRerence( new Object(),  queue  );

如果垃圾回收器检测到 new Object的可达性发生变化后,会将该引用添加到 pending 引用链上,然后有专门的线程 ReferenceHandle线程来将引用加入到引用 链中(入队),也就是应用程序可以从queue中获取到所以垃圾回收器回收的对象的应用,也就是 queue是 垃圾回收器通知应用程序 被引用指向的对象已经被垃圾回收的消息。

ReferenceHandler

上面提到jvm会将要处理的对象设置到pending对象当中,因此肯定有一个线程来进行不断的enqueue操作,此线程即引用处理器线程,其优先级为MAX_PRIORITY,即最高.相应的启动过程为静态初始化创建,可以理解为当任何使用到Reference对象或类时,此线程即会被创建并启动.相应的代码如下所示:

private static class ReferenceHandler extends Thread {

    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        for (;;) {

            Reference r;
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    Reference rn = r.next;
                    pending = (rn == r) ? null : rn;
                    r.next = r;
                } else {
                    try {
                        lock.wait();
                    } catch (InterruptedException x) { }
                    continue;
                }
            }

            // Fast path for cleaners
            if (r instanceof Cleaner) {
                ((Cleaner)r).clean();
                continue;
            }

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

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    /* If there were a special system-only priority greater than
     * MAX_PRIORITY, it would be used here
     */
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
}

handler的wait是jvm在gc的时候唤醒的,大概逻辑就是 gc的某个阶段检查到 oop的可达性变化后把jvm内维护引用的链表链接到到pending后 执行了notify

get()

public T get() {
    return this.referent;
}

clear()

public void clear() {
    this.referent = null;
}

清除引用对象所引用的原对象,这样通过get()方法就不能再访问到原对象了.从相应的设计思路来说,既然都进入到queue对象里面,就表示相应的对象需要被回收了,因为没有再访问原对象的必要.

这个方法只会被代码调用, 垃圾收集器会直接清楚引用的对象,不需要调用这个方法。

  • WeakReference对象进入到queue之后,相应的referent为null.
  • SoftReference对象,如果对象在内存足够时,不会进入到queue,自然相应的reference不会为null.如果需要被处理(内存不够或其它策略),则置相应的referent为null,然后进入到queue.
  • FinalReference对象,因为需要调用其finalize对象,因此其reference即使入queue,其referent也不会为null,即不会clear掉.
  • PhantomReference对象,因为本身get实现为返回null.因此clear的作用不是很大.因为不管enqueue还是没有,都不会清除掉.

isEnqueued()

查看引用是否入列了,当 引用对象在创建的时候没有注册队列,这个方法永远返回NULL

public boolean isEnqueued() {
        
    synchronized (this) {
        return (this.queue != ReferenceQueue.NULL) && (this.next != null);
    }
}

enqueue()

将引用入引用队列,这个方法只会被代码调用, 垃圾收集器会直接操作,不需要调用这个方法。

public boolean enqueue() {
    return this.queue.enqueue(this);
}

流程

场景1
public static void test() throws Exception{
    Object o = new Object();
    WeakReference<Object> wr = new WeakReference<Object>(o);
    System.out.println(wr.get() == null);
    o = null;
    System.gc();
    System.out.println(wr.get() == null);
}

结合代码eg1中的 o = null; 这一句,它使得o对象满足垃圾回收的条件,并且在后边显式的调用了 System.gc(),垃圾收集进行的时候会标记WeakReference所referent的对象o为不可达(使得wr.get()==null),并且通过 赋值给pending,触发ReferenceHandler线程处理pending。

ReferenceHandler线程要做的是将pending对象enqueue,但默认我们所提供的queue,也就是从构造函数传入的是null,实际是使用了ReferenceQueue.NULL,Handler线程判断queue为ReferenceQueue.NULL则不进行操作,只有非ReferenceQueue.NULL的queue才会将Reference进行enqueue。

场景2
public class TestReference {
   	private static ReferenceQueue aQueue = new ReferenceQueue();
   	public static void main(String args) {
        Object a = new Object();   // 代码1
        WeakReference ref = new WeakReference( a, aQueue );  
   	}
}

然后在程序运行过程,内存不断消耗,直至触发垃圾回收操作时,垃圾收集器发现代码1处的 a 所指向的对象,只有 ref引用它,从根路径不可达,,故垃圾回收器,会将 ref 引用加入到 static Reference pending 链表中。【注意,此代码是写在JVM实现中的】

  • 如果pending 为空,则将当前引用(ref) 设置为pengding,并且将 ref对象的next指针指向自己;
  • 如果pending不为空,则将当前的引用(ref)的next指向pengding,然后pengding = 当前的引用ref

优化

另外,由于直接使用referenceQueue,再加上开启线程去监控queue太过麻烦和复杂.可以参考由google guava实现的 FinalizableReferenceQueue 以及相应的FinalizableReference对象.可以简化一点点处理过程.

##

参考网址

gc过程中reference对象的处理

JVM源码分析之 FinalReference 完全解读

java中针对Reference的实现和相应的执行过程

话说ReferenceQueue

java Reference 引用学习总结

理解Java-Reference

JAVA中reference类型简述

Java Reference Objects or How I Learned to Stop Worrying and Love OutOfMemoryError