原创

Java引用类型

Java中一共有4种引用类型(其实还有一些其他的引用类型比如FinalReference):强引用、软引用、弱引用、虚引用。其中强引用就是如下的情况:

Object a=new Object();

obj持有的Object对象的引用就是强引用,在Java中并没有对应的Reference类。

本篇文章主要是分析软引用、弱引用、虚引用的实现,这三种引用类型都是继承于Reference这个类,主要逻辑也在Reference中。

Reference是java中的引用类,它用来给普通对象进行包装,当JVM在GC时,按照引用类型的不同,在回收时执行不同的逻辑。先来看下这个类的继承体系:

file

由图可知,Java存在以下几种引用:

Java中的引用有:

  • 强引用(StrongReference):强引用就是我们平时创建对象,创建数组时的引用。强引用在任何时候都不会被GC回收掉;
  • 软引用(SoftReference):软引用是在系统发生OOM之前才被JVM回收掉。软引用常被用来对于内存敏感的缓存;
  • 弱引用(WeakReference):一旦JVM执行GC,弱引用就会被回收掉;
  • 虚引用(PhantomReference):虚引用主要作为其指向referent被回收时的一种通知机制;
  • FinalReference:用于收尾机制(finalization) 。
    引用实例的几个状态

  • Active:当处于Active状态,GC会特殊处理引用实例,一旦GC检测到其可达性发生变化,GC就会更改其状态。此时分两种情况,如果该引用实例创建时有注册引用队列,则会进入Pending状态,否则会进入Inactive状态。新创建的引用实例为Active。*

  • Pending:当前为pending列表中的一个元素,等待被ReferenceHandler线程消费并加入其注册的引用队列。如果该引用实例未注册引用队列,则永远不会处于这个状态。
  • Enqueued:该引用实例创建时有注册引用队列并且当前处于入队列状态,属于该引用队列中的一个元素。当该引用实例从其注册引用队列中移除后其状态变为Inactive。如果该引用实例未注册引用队列,则永远不会处于这个状态。
  • Inactive:当处于Inactive状态,无需任何处理,一旦变成Inactive状态则其状态永远不会再发生改变。

整体迁移流程图如下:
file

如上的状态是为了更好的理解而虚拟出来的状态,并没有一个字段来描述状态,而是通过queue和next字段来标记的。

  • Active:当实例注册了引用队列,则queue = ReferenceQueue;当实例没有注册引用队列,那么queue = ReferenceQueue.NULL。next = null;
  • Pending:处在这个状态下的实例肯定注册了引用队列,queue = ReferenceQueue。next = this;
  • Enqueued:处在这个状态下的实例肯定注册了引用队列,queue = ReferenceQueue.ENQUEUED,next指向下一个在此队列中的元素,或者如果队列中只有当前对象时为当前对象this;
  • Inactive:queue = ReferenceQueue.NULL;next = this。

    1、Reference

    我们先看下 Reference类及重要属性的定义如下:

    public abstract class Reference<T> {  
      //引用的对象 
      private T referent;   
    
      //回收队列,由使用者在Reference的构造函数中指定   
      volatile ReferenceQueue<? super T> queue;
    
       //当该引用被加入到queue中的时候,该字段被设置为queue中的下一个元素,以形成链表结构    
      volatile Reference  next;   
    
      //在GC时,HotSpot底层会维护一个叫DiscoveredList的链表,存放的是Reference对象,discovered字段指向的就是链表中的下一个元素,由HotSpot设置   
      transient private Reference<T>  discovered; 
    
      //进行线程同步的锁对象    
      static private class Lock { }   
      private static Lock  lock = new Lock();
    
      //等待加入queue的Reference对象,在GC时由JVM设置,会有一个java层的线程(ReferenceHandler)源源不断的从pending中提取元素加入到queue 
      private static Reference<Object> pending = null;  
    }
    

    注意Reference指的是引用对象,而Referent指的是所指对象。

一个Reference对象的生命周期如下:
file

HotSpot在GC时将需要被回收的Reference对象加入到DiscoveredList中,然后将DiscoveredList的元素移动到PendingList中。PendingList的队首元素由Reference类中的pending属性持有。

2、ReferenceHandler

ReferenceHandler的代码实现如下:

private static class ReferenceHandler extends Thread {  
         ...    
        public void run() { 
            while (true) {  
                 tryHandlePending(true);    
            }   
        }   
}   

static boolean tryHandlePending(boolean waitForNotify) {    
        Reference<Object> r;  
        Cleaner c;  
        try {   
            synchronized (lock) {   
                if (pending != null) {  
                     r = pending;   
                     // 如果是Cleaner对象,则记录下来,下面做特殊处理  
                     c = r instanceof Cleaner ? (Cleaner) r : null; 
                     // 指向PendingList的下一个对象 
                     pending = r.discovered;    
                     r.discovered = null;   
                } else {    
                     // 如果pending为null就先等待,当有对象加入到PendingList中时,jvm会执行notify    
                     if (waitForNotify) {   
                         lock.wait();   
                     }  
                     // retry if waited 
                     return waitForNotify;  
                }   
            }   
        }   
        ... 
        // 如果时Cleaner对象,则调用clean方法进行资源回收    
        if (c != null) {    
             c.clean(); 
             return true;   
        }   
        // 将Reference加入到ReferenceQueue,开发者可以通过从ReferenceQueue中poll元素感知到对象被回收的事件。    
        ReferenceQueue<? super Object> q = r.queue;   
        if (q != ReferenceQueue.NULL) 
             q.enqueue(r);  
        return true;    
 }

源源不断的从PendingList中获取元素,然后加入到ReferenceQueue中,开发者可以通过调用ReferenceQueue的poll()方法来感知对象被回收的事件。

另外需要注意的是,对于Cleaner类型(继承自虚引用)的对象会有额外的处理:在其指向的对象被回收时,会调用clean()方法,该方法主要是用来做对应的资源回收,在堆外内存DirectByteBuffer中就是用Cleaner进行堆外内存的回收,这也是虚引用在java中的典型应用,后面会详细介绍。

3、ReferenceQueue

ReferenceQueue是引用队列,垃圾收集器在检测到适当的可达性更改后将已注册的引用对象追加到该队列。

public class ReferenceQueue<T> {

    public ReferenceQueue() { }

    // 内部类Null类继承自ReferenceQueue,覆盖了enqueue方法返回false
    private static class Null extends ReferenceQueue<Object> {
        boolean enqueue(Reference<?> r) {
            return false;
        }
    }

    // ReferenceQueue.NULL和ReferenceQueue.ENQUEUED都是内部类Null的新实例
    static final ReferenceQueue<Object> NULL = new Null();
    static final ReferenceQueue<Object> ENQUEUED = new Null();

    // 静态内部类,作为锁对象
    private static class Lock { };
    private final Lock lock = new Lock();<br>
    // 引用链表的头节点
    private volatile Reference<? extends T> head;
    // 引用队列长度,入队则增加1,出队则减少1
    private long queueLength = 0;  

    // 入队操作,只会被Reference实例调用
    boolean enqueue(Reference<? extends T> r) {
        // 加锁
        synchronized (lock) {
            // Check that since getting the lock this reference hasn't already been
            // enqueued (and even then removed)
            // 如果引用实例持有的队列为ReferenceQueue.NULL或者ReferenceQueue.ENQUEUED,则入队失败返回false
            ReferenceQueue<?> queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            // Self-loop end, so if a FinalReference it remains inactive.
            // 如果链表没有元素,则此引用实例直接作为头节点,否则把前一个引用实例作为下一个节点
            r.next = (head == null) ? r : head;
            // 当前实例更新为头节点,也就是每一个新入队的引用实例都是作为头节点,已有的引用实例会作为后继节点
            head = r;
            // 队列长度增加1
            queueLength++;
            // Update r.queue *after* adding to list, to avoid race
            // with concurrent enqueued checks and fast-path poll().
            // Volatiles ensure ordering.
            // 当前引用实例已经入队,那么它本身持有的引用队列实例置为ReferenceQueue.ENQUEUED
            r.queue = ENQUEUED;
            // 特殊处理FinalReference,VM进行计数
            if (r instanceof FinalReference) {
                VM.addFinalRefCount(1);
            }
            // 唤醒所有等待的线程
            lock.notifyAll();
            return true;
        }
    }

    // 引用队列的poll操作,此方法必须在加锁情况下调用
    private Reference<? extends T> reallyPoll() { 
        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节点为自身,那么队列中只有当前这个对象一个元素
            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();
        }
    }

    // ...
}

从源码上看,实际上ReferenceQueue只是名义上的引用队列,它只保存了Reference链表的头(head)节点,并且提供了出队、入队等操作,而Reference实际上本身提供单向链表的功能,也就是Reference通过属性next构建单向链表,而链表的操作通过ReferenceQueue这个类来完成。

正文到此结束