转载

JDK源码阅读-Reference

Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象。

如果只提供强引用,我们就很难写出“这个对象不是很重要,如果内存不足GC回收掉也是可以的”这种语义的代码。Java在1.2版本中完善了引用体系,提供了4中引用类型:强引用,软引用,弱引用,虚引用。使用这些引用类型,我们不但可以控制垃圾回收器对对象的回收策略,同时还能在对象被回收后得到通知,进行相应的后续操作。

总体结构

Reference 类是所有引用类型的基类,Java提供了具体引用类型的具体实现:

SoftReference
WeakReference
PhantomReference
FinalReference

因为默认的引用就是强引用,所以没有强引用的Reference实现类。

JDK源码阅读-Reference

Reference的核心

Java的多种引用类型实现,不是通过扩展语法实现的,而是利用类实现的,Reference类表示一个引用,其核心代码就是一个成员变量 reference

public abstract class Reference<T> {
	private T referent; // 会被GC特殊对待
    
    // 获取Reference管理的对象
    public T get() {
        return this.referent;
    }
    
    // ...
}

如果JVM没有对这个变量做特殊处理,它依然只是一个普通的强引用,之所以会出现不同的引用类型,是因为JVM垃圾回收器硬编码识别 SoftReferenceWeakReferencePhantomReference 等这些具体的类,对其 reference 变量进行特殊对象,才有了不同的引用类型的效果。

上文提到了 Reference 及其子类有两大功能:

  1. 实现特定的引用类型
  2. 用户可以对象被回收后得到通知

第一个功能已经解释过了,第二个功能是如何做到的呢?

一种思路是在新建一个 Reference 实例是,添加一个回调,当 java.lang.ref.Reference#referent 被回收时,JVM调用该回调,这种思路比较符合一般的通知模型,但是对于引用与垃圾回收这种底层场景来说,会导致实现复杂,性能不高的问题,比如需要考虑在什么线程中执行这个回调,回调执行阻塞怎么办等等。

所以 Reference 使用了一种更加原始的方式来做通知,就是把引用对象被回收的 Reference 添加到一个队列中,用户后续自己去从队列中获取并使用。

理解了设计后对应到代码上就好理解了, Reference 有一个 queue 成员变量,用于存储引用对象被回收的 Reference 实例:

public abstract class Reference<T> {
    // 会被GC特殊对待
	private T referent; 
    // reference被回收后,当前Reference实例会被添加到这个队列中
    volatile ReferenceQueue<? super T> queue;
    
    // 只传入reference的构造函数,意味着用户只需要特殊的引用类型,不关心对象何时被GC
    Reference(T referent) {
        this(referent, null);
    }
	
    // 传入referent和ReferenceQueue的构造函数,reference被回收后,会添加到queue中
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
    
    // ...
}

Reference的状态

Reference 对象是有状态的。一共有4中状态:

  1. Active :新创建的实例的状态,由垃圾回收器进行处理,如果实例的可达性处于合适的状态,垃圾回收器会切换实例的状态为 Pending 或者 Inactive 。如果Reference注册了ReferenceQueue,则会切换为 Pending ,并且Reference会加入 pending-Reference 链表中,如果没有注册ReferenceQueue,会切换为 Inactive
  2. Pending :在 pending-Reference 链表中的Reference的状态,这些Reference等待被加入ReferenceQueue中。
  3. Enqueued :在ReferenceQueue队列中的Reference的状态,如果Reference从队列中移除,会进入 Inactive 状态
  4. Inactive :Reference的最终状态

Reference 对象图如下:

JDK源码阅读-Reference

除了上文提到的 ReferenceQueue ,这里出现了一个新的数据结构: pending-Reference 。这个链表是用来干什么的呢?

上文提到了, reference 引用的对象被回收后,该 Reference 实例会被添加到 ReferenceQueue 中,但是这个不是垃圾回收器来做的,这个操作还是有一定逻辑的,如果垃圾回收器还需要执行这个操作,会降低其效率。从另外一方面想, Reference 实例会被添加到 ReferenceQueue 中的实效性要求不高,所以也没必要在回收时立马加入 ReferenceQueue

所以垃圾回收器做的是一个更轻量级的操作:把 Reference 添加到 pending-Reference 链表中。 Reference 对象中有一个pending成员变量,是静态变量,它就是这个 pending-Reference 链表的头结点。要组成链表,还需要一个指针,指向下一个节点,这个对应的是 java.lang.ref.Reference#discovered 这个成员变量。

可以看一下代码:

public abstract class Reference<T> {
    // 会被GC特殊对待
	private T referent; 
    // reference被回收后,当前Reference实例会被添加到这个队列中
    volatile ReferenceQueue<? super T> queue; 
 	
    // 全局唯一的pending-Reference列表
    private static Reference<Object> pending = null;
    
    // Reference为Active:由垃圾回收器管理的已发现的引用列表(这个不在本文讨论访问内)
    // Reference为Pending:在pending列表中的下一个元素,如果没有为null
    // 其他状态:NULL
    transient private Reference<T> discovered;  /* used by VM */
    // ...
}

ReferenceHandler线程

通过上文的讨论,我们知道一个Reference实例化后状态为Active,其引用的对象被回收后,垃圾回收器将其加入到 pending-Reference 链表,等待加入ReferenceQueue。这个过程是如何实现的呢?

这个过程不能对垃圾回收器产生影响,所以不能在垃圾回收线程中执行,也就需要一个独立的线程来负责。这个线程就是 ReferenceHandler ,它定义在 Reference 类中:

// 用于控制垃圾回收器操作与Pending状态的Reference入队操作不冲突执行的全局锁
// 垃圾回收器开始一轮垃圾回收前要获取此锁
// 所以所有占用这个锁的代码必须尽快完成,不能生成新对象,也不能调用用户代码
static private class Lock { };
private static Lock lock = new Lock();

private static class ReferenceHandler extends Thread {

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

    public void run() {
        // 这个线程一直执行
        for (;;) {
            Reference<Object> r;
            // 获取锁,避免与垃圾回收器同时操作
            synchronized (lock) {
                // 判断pending-Reference链表是否有数据
                if (pending != null) {
                    // 如果有Pending Reference,从列表中取出
                    r = pending;
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // 如果没有Pending Reference,调用wait等待
                    // 
                    // wait等待锁,是可能抛出OOME的,
                    // 因为可能发生InterruptedException异常,然后就需要实例化这个异常对象,
                    // 如果此时内存不足,就可能抛出OOME,所以这里需要捕获OutOfMemoryError,
                    // 避免因为OOME而导致ReferenceHandler进程静默退出
                    try {
                        try {
                            lock.wait();
                        } catch (OutOfMemoryError x) { }
                    } catch (InterruptedException x) { }
                    continue;
                }
            }

            // 如果Reference是Cleaner,调用其clean方法
            // 这与Cleaner机制有关系,不在此文的讨论访问
            if (r instanceof Cleaner) {
                ((Cleaner)r).clean();
                continue;
            }

            // 把Reference添加到关联的ReferenceQueue中
            // 如果Reference构造时没有关联ReferenceQueue,会关联ReferenceQueue.NULL,这里就不会进行入队操作了
            ReferenceQueue<Object> q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
        }
    }
}

ReferenceHandler 线程是在Reference的static块中启动的:

static {
    // 获取system ThreadGroup
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread handler = new ReferenceHandler(tg, "Reference Handler");

    // ReferenceHandler线程有最高优先级
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
}

综上, ReferenceHandler 是一个最高优先级的线程,其逻辑是从 Pending-Reference 链表中取出Reference,添加到其关联的 Reference-Queue 中。

ReferenceQueue

Reference-Queue 也是一个链表:

public class ReferenceQueue<T> {
    private volatile Reference<? extends T> head = null;
    // ...
}
// ReferenceQueue中的这个锁用于保护链表队列在多线程环境下的正确性
static private class Lock { };
private Lock lock = new Lock();

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
    synchronized (lock) {
		// 判断Reference是否需要入队
        ReferenceQueue<?> queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
        
        // Reference入队后,其queue变量设置为ENQUEUED
        r.queue = ENQUEUED;
        // Reference的next变量指向ReferenceQueue中下一个元素
        r.next = (head == null) ? r : head;
        head = r;
        queueLength++;
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(1);
        }
        lock.notifyAll();
        return true;
    }
}

通过上面的代码,可以知道 java.lang.ref.Reference#next 的用途了:

public abstract class Reference<T> {
    /* When active:   NULL
     *     pending:   this
     *    Enqueued:   指向ReferenceQueue中的下一个元素,如果没有,指向this
     *    Inactive:   this
     */
    Reference next;
    
    // ...
}
原文  http://imushan.com/2018/08/19/java/language/JDK源码阅读-Reference/
正文到此结束
Loading...