聊聊Java的引用类型(二)
2019-12-24 | 分类 Java | 标签 Java Reference 垃圾回收

前言

在之前的文章《聊聊Java的引用类型(一)》中,我们已经介绍了Java中的四种引用类型。在本文中,我们将深入JavaReference类的源码实现,看看Java的ReferenceReferenceQueue以及垃圾回收器之间是如何协作,使Reference对象被放入ReferenceQueue

Reference

上一篇文章中介绍的SoftReferenceWeakReference以及PhantomReference类都继承了Reference这个类。在基类Reference中定义了两个队列,用于处理引用对象和ReferenceQueue之间的交互。为了确定一个引用对象什么时候进入队列,Reference中定义了四种状态:ActivePendingEnqueuedInactive

Active
被标记为active状态的引用对象会被垃圾回收器识别并处理。当垃圾回收器确定这个引用对象的状态发生变更的时候,会将这个引用对象的状态变更到合适的状态,比如:变更到pending状态或者inactive状态,具体转换成pending状态还是inactive状态取决于引用对象在创建的时候是否注册了引用队列。新创建的引用对象状态默认是active
Pending
被放入pending-reference队列的对象,等待被reference-handler线程处理。对于未注册(创建的时候没有指定引用队列)的引用对象,不会进入这个状态。
Enqueued
已经入队的注册引用对象的状态是enqueued,当对象从引用队列中删除以后会进入inactive状态。对于未注册的引用对象,不会进入这个状态。
Inactive
最终状态,当一个对象进入inactive状态以后就进入终态,状态不会再变化。

状态转换

Reference类本身没有通过一个字段直接表示这些状态,而是通过两个字段queuenext来编码这些状态。每个状态和这两个字段之间的对应关系如下:

状态编码

为了让并发执行的垃圾收集器可以不依赖于应用程序中的引用处理线程,Reference还引入了一个discovered成员变量。当垃圾收集器发现了处于active状态的引用对象的时候,会将这些引用对象放入discovered维护的列表中。Reference除了使用discovered来维护active状态的引用对象外,它还被复用来维护pending状态下的引用对象的 pending列表

Pending处理线程

前面提到,垃圾收集器通过discovered成员变量维护 active 状态的引用对象列表。当垃圾回收器发现某个引用对象指向的实际对象达到了某种可达性(在《聊聊Java的引用类型(一)》中提到的三种可达性)以后,如果这个引用对象在创建的时候注册了ReferenceQueue,则会将这个引用对象从discovered列表中摘除,并放到 pending 列表 中。

/* When active:   next element in a discovered reference list maintained by GC (or this if last)
 *     pending:   next element in the pending list (or null if last)
 *   otherwise:   NULL
 */
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;

Reference通过pending静态成员变量指向 pending列表,在列表中通过复用discovered变量来维护列表中成员的关联关系。

pending-list

Reference中通过一个pending列表处理线程ReferenceHandlerpending列表 中取出处于 pending 状态的引用对象并将该引用对象入队。线程 ReferenceHandler通过静态初始化并启动,启动代码如下:

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();

    // provide access in SharedSecrets
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

通过设置一些线程属性,然后调用start()方法启动pending列表处理线程。ReferenceHandler的实现如下:

private static class ReferenceHandler extends Thread {

    private static void ensureClassInitialized(Class<?> clazz) {
        try {
            Class.forName(clazz.getName(), true, clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
        }
    }

    static {
        // pre-load and initialize InterruptedException and Cleaner classes
        // so that we don't get into trouble later in the run loop if there's
        // memory shortage while loading/initializing them lazily.
        ensureClassInitialized(InterruptedException.class);
        ensureClassInitialized(Cleaner.class);
    }

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

    public void run() {
        while (true) {
            tryHandlePending(true);
        }
    }
}

ReferenceHandlerrun()方法中,调用tryHandlePending(true)执行具体的 pending列表 处理逻辑:

static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    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
                    pending = r.discovered;
                    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;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }

        // Fast path for cleaners
        if (c != null) {
            c.clean();
            return true;
        }

        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

处理过程也比较清晰,首先判断pending静态变量是否为null,如果为null则表示当前没有需要处理的 pending 对象,调用lock.wait()方法等待垃圾回收器的通知。如果 pending列表 不为空,则将 pending列表 中头部的引用对象取出,并将pending指针指向由discovered维护的下一个 pending 对象。然后获取引用对象r所注册的引用队列r.queue,调用q.enqueue()方法进行入队操作。下面,我们来看下ReferenceQueue这个队列是如何实现enqueue()入队操作的。

ReferenceQueue

ReferenceQueueReference是联系很紧密的两个类,虽然ReferenceQueue从名字上看是一个队列,但是实际上ReferenceQueue实现的队列数据结构和我们通常了解到的队列数据结构还有点不一样,ReferenceQueue队列的实现依赖于Reference类。

下面,我们先来看下ReferenceQueue定义了哪些队列操作。在ReferenceQueue中总共实现了四个队列操作方法,分别对应了一个入队操作enqueue()以及三个出队操作poll()remove()

boolean enqueue(Reference<? extends T> r);
public Reference<? extends T> poll();
public Reference<? extends T> remove(long timeout) throws IllegalArgumentException, InterruptedException;
public Reference<? extends T> remove();

首先来看下ReferenceQueue的入队操作enqueue的实现:

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
    synchronized (lock) {
        // Check that since getting the lock this reference hasn't already been
        // enqueued (and even then removed)
        ReferenceQueue<?> queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
        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;
    }
}

可以看到,在ReferenceQueue的实现中,实际并没有一个队列数据结构来存储Reference对象,而是通过操作Reference对象的next域指针以及维护一个head头指针将Reference对象串联起来用来表示队列。通过正确设置Reference对象的queue成员变量的值来表示引用对象当前是在队列中还是已经出队:如果引用对象还未在队列中,则Reference对象的queue字段值为引用对象注册的ReferenceQueue对象的实例;如果引用对象已经入队,则queue的值为ENQUEUED常量。入队操作通过修改ReferenceQueuehead值来实现。

队列操作

引用对象入队以后,通过locknotifyAll()方法唤醒所有阻塞在remove()方法上的应用程序线程。remove()方法的实现如下:

public Reference<? extends T> remove() throws InterruptedException {
    return remove(0);
}

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;
        long start = (timeout == 0) ? 0 : System.nanoTime();
        for (;;) {
            lock.wait(timeout);
            r = reallyPoll();
            if (r != null) return r;
            if (timeout != 0) {
                long end = System.nanoTime();
                timeout -= (end - start) / 1000_000;
                if (timeout <= 0) return null;
                start = end;
            }
        }
    }
}

remove()方法有两个实现版本:一个是定时阻塞版本,一个是永久阻塞版本。remove()的内部实现通过lock.wait()方法和enqueue()方法中的lock.notifyAll()配合使用来进行线程间同步,所以实际上remove()enqueue()实现的是一个 生产者-消费者 模型。

生产者-消费者模型

remove()内部通过reallyPoll()来实现出队操作:

private Reference<? extends T> reallyPoll() {       /* Must hold lock */
    Reference<? extends T> r = head;
    if (r != null) {
        head = (r.next == r) ?
            null :
            r.next; // Unchecked due to the next field having a raw type in Reference
        r.queue = NULL;
        r.next = r;
        queueLength--;
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(-1);
        }
        return r;
    }
    return null;
}

通过设置引用对象的queue = NULL以及修改head的值将引用对象出队。通过分析reallyPoll()方法和enqueue()方法中的出入队逻辑,会发现虽然ReferenceQueue名字叫队列,实际操作的时候修改head值的过程是一个出入栈的操作。

最后一个是poll()方法,实现比较简单,内部通过调用reallyPoll()来实现,是remove()的非阻塞版本。

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

到这里,差不多已经回答了引用对象是怎么被放入ReferenceQueue中的,下面是给出整个过程的图示:

整个流程

总结

本文主要介绍了Java的实现是如何将Reference对象放入引用队列ReferenceQueue中的,结合上一篇《聊聊Java的引用类型(一)》,我们从概念到使用再到原理介绍了Java的Reference类和三种特殊的引用类型。

TOP