從JDK原始碼理解java引用

bmilk發表於2020-07-14

目錄

  • java中的引用
  • 引用佇列
  • 虛引用、弱引用、軟引用的實現
  • ReferenceHandler執行緒
  • 引用佇列的實現
  • 總結
  • 參考資料

java中的引用

JDK 1.2之後,把物件的引用分為了四種型別,分別為:強引用、軟應用、弱引用和虛引用,以方便控制java物件的生命週期。

  1. 強引用

強引用是工作開發中使用最多的引用型別。比如宣告一個字串變數String str="abc"只要物件與強引用關聯,JVM就不會回收這個物件,即不會回收這個區域
的記憶體。
如果我們希望回收abc這個物件,那麼就需要顯示的將str設定為null,即str=null。那麼jvm就會在下一次gc時回收這部分記憶體區域。

2.軟引用

軟引用可以用來描述一些非必須的物件,java中使用SoftRefence來表示軟引用。對於軟引用的生命如下:

  SoftRefence<String> str = new SoftRefence<String>(new String("abc"));
  或者
  SoftReference<String> str=new SoftReference<>("abc");

對於軟引用關聯的物件,只要系統記憶體足夠,就不會回收這部分記憶體,只有當系統要發生記憶體洩漏之前才會將只與軟引用關聯的物件進行回收,當回收完之後記憶體還不足時
會丟擲記憶體溢位的異常。

  1. 弱引用

弱引用用來描述非必須的物件。使用方法如下:

  WeakReference<String> str=new WeakReference<>("abc");
  或者
  WeakReference<String> str=new WeakReference<String>(new String("abc"));

對於只使用弱引用描述的物件,這個物件可以通過弱引用用找到,但是他只能存活到下一次gc之前,也就是說,只被弱引用關聯的物件,在下一次gc時,會被垃圾回收掉。
驗證如下:

public class Weak {
    public static void main(String[] args) throws InterruptedException {
        String str="abc";
        WeakReference<String> weakStr=new WeakReference<>(str);
        System.out.println("=======當物件同時被強引用和弱引用關聯,執行一次gc======");
        System.gc();
        System.out.println(weakStr.get());
        System.out.println("========當物件只與弱引用關聯======");
        str = null;
        System.out.println(weakStr.get());
        System.out.println("========再次執行gc======");
        System.gc();
        Thread.sleep(10000);
        System.out.println(weakStr);
        System.out.println(weakStr.get());
    }
}

結果如下:

=======當物件同時被強引用和弱引用關聯,執行一次gc======
abc
========當物件只與弱引用關聯======
abc
========再次執行gc======
java.lang.ref.WeakReference@1b6d3586
abc

咦沒回收!!!再來看個例子

public class Weak {
    public static void main(String[] args) throws InterruptedException {
        Integer i=new Integer(10);
        WeakReference<Integer> weakStr=new WeakReference<>(i);
        System.out.println("=======當物件同時被強引用和弱引用關聯,執行一次gc======");
        System.gc();
        System.out.println(weakStr.get());
        System.out.println("========當物件只與弱引用關聯======");
        i = null;
        System.out.println(weakStr.get());
        System.out.println("========再次執行gc======");
        System.gc();
        Thread.sleep(10000);
        System.out.println(weakStr);
        System.out.println(weakStr.get());
    }
}

結果如下:

=======當物件同時被強引用和弱引用關聯,執行一次gc======
10
========當物件只與弱引用關聯======
10
========再次執行gc======
java.lang.ref.WeakReference@1b6d3586
null

回收了!!!為什麼??

  1. 虛引用

虛引用是引用關係中最弱的一種引用,也被稱為幽靈引用或者幻影引用。一個物件是否存在虛引用,完全不會對其生存時間構成影響,也無法通過虛引用來獲取一個物件例項,
為物件設定虛引用的目的是為了能夠在物件被回收器回收時得到一個通知。
虛引用的使用必須和一個引用佇列繫結,使用舉例如下:

  PhantomReference<String> str= new PhantomReference<>("abc",new ReferenceQueue<>());

引用佇列

引用佇列提供了一種可以直觀瞭解垃圾回收的方式。引用佇列可以和軟引用、弱引用和虛引用搭配使用,當JVM進行垃圾收集的時候會將引用物件加入到引用佇列中,
即:在引用佇列中的節點(引用型別的物件)所關聯的物件已經被回收。例子如下:

public static void main(String[] args) {
    Object object=new Object();
    ReferenceQueue<Object> objectReferenceQueue=new ReferenceQueue<>();
    WeakReference<Object> objectWeakReference=new WeakReference<>(object,objectReferenceQueue);
    object=null;
    System.gc();
    System.out.println(objectWeakReference.get());
    System.out.println(objectReferenceQueue.poll());
}

輸出結果如下:

objectWeakReference.get()====null
objectWeakReference====java.lang.ref.WeakReference@1b6d3586
objectReferenceQueue.poll()====java.lang.ref.WeakReference@1b6d3586

上面的例子中object物件關聯了一個弱引用objectWeakReference。從輸出結果來看,當object物件垃圾回收之後,objectWeakReference物件被加入到了引用佇列當中。

虛引用、弱引用、軟引用的實現

虛引用、弱引用、軟引用都繼承自Reference(引用類)抽象類,另外還有一個FinaReference也是繼承自這個類。他們之間的類圖如下:

從JDK原始碼理解java引用
圖1  Reference的類圖

引用類的實現

引用類定義了所有引用物件的通用的操作,這個類和垃圾回收機制緊密象關,是一個抽象類不能被直接例項化,類的定義成如下:

public abstract class Reference<T> {
   
    //當建立一個引用物件並繫結一個強引用物件時,
    //就是對這個欄位賦值,將這個欄位指向強引用的物件
    //GC特殊處理的物件
    private T referent;         /* Treated specially by GC */

    // reference物件關聯的引用佇列。如果物件被回收,這個佇列將作為通知的回撥佇列。
    // 當reference物件關聯的物件將要被回收時,reference物件將會被放進引用佇列,就可以從引用佇列來監控垃圾回收情況
    volatile ReferenceQueue<? super T> queue;

    /* When active:   NULL
     *     pending:   this
     *    Enqueued:   next reference in queue (or this if last)
     *    Inactive:   this
     */
    // 這個欄位用於在引用佇列中構建單項鍊表。在引用佇列中,佇列中的每個元素是一個引用型別,這個欄位用於指向下一個節點
    /**
     * 當引用物件處於不同狀態時,這個欄位的值不同
     * Active:NULL
     * Pending:THIS
     * Enqueue:NEXT
     * Inactive:THIS
     */
    @SuppressWarnings("rawtypes")
    volatile Reference next;

    /* 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
     */
    // 基於狀態不同表示的連結串列不同
    // 主要是pending-reference列表的下一個元素
    // 需要和 pending屬性搭配使用,
    transient private Reference<T> discovered;  /* used by VM */
    
    /* Object used to synchronize with the garbage collector.  The collector
     * must acquire this lock at the beginning of each collection cycle.  It is
     * therefore critical that any code holding this lock complete as quickly
     * as possible, allocate no new objects, and avoid calling user code.
     */
    // 這個鎖用於垃圾收集器的同步,收集器在垃圾收集之前必須獲取鎖,所以任何獲得整個鎖的程式碼都必須儘快完成
    // 儘量不建立新的物件,儘量不呼叫使用者程式碼。
    static private class Lock { }
    private static Lock lock = new Lock();


    /* 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.
     */
    /**
     * 一個處於pending狀態連結串列,他們都在等待進入引用佇列,
     * 垃圾收集器會向該列表新增reference物件,而ReferenceHandler執行緒會處理這個連結串列
     * 連結串列的節點使用discovered屬性代表連結串列的下一個節點的位置
     * 就相當於連結串列的head和next
     * 在對連結串列操作時必須獲得上面的鎖物件,避免想成不安全
     * 因為可能有這裡在嘗試將pending狀態的引用物件加入引用佇列,jvm需要進行垃圾回收發現可達性改變的物件
     */
    private static Reference<Object> pending = null;

    
    // some code
}

上面的程式碼中有描述了四個引用的狀態,分別是ActivePendingEnqueuedInactive,但是在類的定義中是沒有一個屬性來專門表示這個狀態的。
它通過queue屬性和next屬性聯合起來表示。

  • Active:引用指向的物件還存在強引用,對於Active的Reference,它的next為null;如果在建立引用的時候傳入了ReferenceQueue,那麼queue為傳入的ReferenceQueue,否則為null
  • Pending:等待著加入到該Reference在建立時註冊的ReferenceQueue,它的next為自己
  • Enqueued:表示該Reference已經加入到其建立時註冊的ReferenceQueue,它的nextReferenceQueue中的下個元素
  • Inactive:當從引用佇列中poll出來之後將會處於這個狀態,如果沒有註冊引用佇列,會直接到這個狀態。一旦進入此狀態,不會對該Reference做任何事,該狀態是最終態

構造方法:

    /* -- Constructors -- */

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

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

總體來說,構造方法只是對reference欄位和queue欄位賦值,如果沒有傳入引用佇列,則為ReferenceQueue.NULL

其他一些例項方法:

// 獲取關聯的例項物件
@HotSpotIntrinsicCandidate
public T get() {
     return this.referent;
}

// 設定reference欄位為null
public void clear() {
     this.referent = null;
}

// 判斷是否處於enqeued狀態
public boolean isEnqueued() {
     return (this.queue == ReferenceQueue.ENQUEUED);
}

// 將當前的引用放入引用佇列,此時會將reference設定為null
public boolean enqueue() {
     this.referent = null;
     return this.queue.enqueue(this);
}

// 禁止clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
     throw new CloneNotSupportedException();
}

// 確保給定的引用例項是強可達的
@ForceInline
public static void reachabilityFence(Object ref) {
}

ReferenceHandler執行緒

reference類的實現中還定義了一個繼承了Thread類靜態內部類。並且在reference還有一個靜態程式碼塊啟動了一個執行緒,用來處理pending鏈中的物件。

ReferenceHandler類定義:

private static class ReferenceHandler extends Thread {

    // 確保對應的類已經載入並初始化
    // 其實這列確保的就是InterruptedException類和Cleaner類
    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() {
        // 死迴圈處理pending狀態的物件
        while (true) {
            tryHandlePending(true);
        }
    }
}

tryHandlePending()方法

/**
 * Try handle pending {@link Reference} if there is one.<p>
 * Return {@code true} as a hint that there might be another
 * {@link Reference} pending or {@code false} when there are no more pending
 * {@link Reference}s at the moment and the program can do some other
 * useful work instead of looping.
 *
 * @param waitForNotify if {@code true} and there was no pending
 *                      {@link Reference}, wait until notified from VM
 *                      or interrupted; if {@code false}, return immediately
 *                      when there is no pending {@link Reference}.
 * @return {@code true} if there was a {@link Reference} pending and it
 *         was processed, or we waited for notification and either got it
 *         or thread was interrupted before being notified;
 *         {@code false} otherwise.
 */
// 處理pending狀態的reference,如果有的話。
// 如果連結串列的後續還有處於pending狀態的節點則返回true
// 如果後續沒有處於pending狀態的節點,則返=返回false;
static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            // pending定義在reference類中:private static Reference<Object> pending = null;
            // 這個欄位代表了一個reference型別的連結串列,一系列等待進入佇列的引用物件,
            // 垃圾收集器會向該列表新增reference物件,而ReferenceHandler執行緒會處理這個連結串列
            // 這個列表由上面的鎖物件保護,避免執行緒不安全的事件。
            // pengding為靜態屬性,全域性只有一份

            //pending不為null則這個連結串列中存在等待入隊的元素
            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
                /**
                 * 這裡使用discovered表示垃圾收集器發現的處於pengding狀態的一個節點,
                 * 可以將起理解為連結串列的節點的next屬性,代表指向下一個節點的指標
                 * 這裡代表指向下一個處於pengding狀態的引用物件
                 *
                 * 這個欄位定義在reference類中:transient private Reference<T> discovered;  /* used by VM */
                 * 這個欄位由jvm維護
                 */
                pending = r.discovered;
                
                // 將獲取到的pending節點的下一個節點改為null
                // enqueue和Inactive狀態的節點的discovered屬性值為null
                r.discovered = null;
            } else {
                // The waiting on the lock may cause an OutOfMemoryError
                // because it may try to allocate exception objects.

                // 這塊的返回值很有意思
                // 如果傳入的waitForNotify值是true,當發現pending為null時,會執行Object.wait方法
                // 這時會釋放自己持有的lock鎖,然後由JVM發現物件的可達性發生變化並將物件加入pending佇列時
                // 會使用到Object.notifyAll()方法從阻塞中喚醒,
                // 那麼此時pending佇列中就已經有了元素,可以繼續迴圈將pending狀態的節點放入queue

                // 如果waitForNotify是false,那麼如果為null則直接返回null,代表後續沒有pending狀態的節點
                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;
    }
    // 對於獲取的reference物件,獲取其引用佇列
    // 執行入隊方法,入隊方法在引用佇列中實現。
    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);

    //這裡直接返回true,代表後續有等待pending的節點,但其實有沒有無所謂,下一次再進來的時候還是會判斷pending狀態的節點存在不存在。
    return true;
}

ReferenceHandler執行緒的啟動

ReferenceHandler執行緒的啟動在reference類的靜態程式碼塊中,ReferenceHandler是以守護執行緒的方式啟動。

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    //建立ReferenceHandler物件
    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();

    // 注意這裡覆蓋了全域性的jdk.internal.misc.JavaLangRefAccess實現
    // 但這裡不太明白這段程式碼是為了做什麼
    // provide access in SharedSecrets
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

引用佇列的實現

引用佇列因為同時有ReferenceHandler執行緒在使用也由使用者執行緒在使用,因此需要進行同步。

引用佇列的本質是一個單連結串列,並且連結串列的的節點就是reference物件,定義如下:

/**
 * Reference queues, to which registered reference objects are appended by the
 * garbage collector after the appropriate reachability changes are detected.
 *
 * @author   Mark Reinhold
 * @since    1.2
 */
// 引用佇列,他在例項化引用物件的時候被註冊到引用物件當中,當引用物件關聯的物件的可達性發生變化時,由`ReferenceHandler`執行緒將其加入註冊的引用佇列
public class ReferenceQueue<T> {

    /**
     * Constructs a new reference-object queue.
     */
    public ReferenceQueue() { }

    private static class Null<S> extends ReferenceQueue<S> {
        //重寫enqueue方法,對於在建立引用物件時未指定引用佇列的物件,引用佇列就是NULL
        boolean enqueue(Reference<? extends S> r) {
            return false;
        }
    }
    
    // 對於在建立引用物件時未指定引用佇列的物件,引用佇列就是這個欄位
    static ReferenceQueue<Object> NULL = new Null<>();
    // 對於已經咋引用佇列中的物件,其關聯的引用佇列就是這個屬性
    static ReferenceQueue<Object> ENQUEUED = new Null<>();

    static private class Lock { };
    private Lock lock = new Lock();
    //引用佇列的頭結點。引用佇列訪問的入口有且僅有一個頭結點
    private volatile Reference<? extends T> head = null;
    private long queueLength = 0;

    // 將引用物件加入佇列
    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;
            // 引用物件關聯的物件為NULL或者ENQUEUED代表其已經出了對列或者已經在佇列中,直接返回false
            // 這裡為什麼不說NULL可能是引用物件在建立時沒有註冊引用佇列呢?
            // 因為在呼叫入隊方法前已經進行了判斷,
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            // 將引用物件關聯的引用佇列改為ENQUEUED,代表其已經入隊
            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;
        }
    }
 
    // 引用物件出隊的方法,在呼叫這個方法之前必須獲取鎖
    private Reference<? extends T> reallyPoll() {       /* Must hold lock */
        Reference<? extends T> r = head;
        // head != null,代表佇列不為null
        if (r != null) {
            @SuppressWarnings("unchecked")
            Reference<? extends T> rn = r.next;
            //移動head位置,並將去出隊的物件賦值給r
            head = (rn == r) ? null : rn;
            // 設定出隊的物件關聯的引用佇列為null
            r.queue = NULL;
            // 引用物件的下一個節點指向自己,代表引用已經失效,引用的狀態將轉移到Inactive
            r.next = r;
            queueLength--;
            //解釋在後面
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(-1);
            }
            return r;
        }
        return null;
    }

    /**
     * Polls this queue to see if a reference object is available.  If one is
     * available without further delay then it is removed from the queue and
     * returned.  Otherwise this method immediately returns <tt>null</tt>.
     *
     * @return  A reference object, if one was immediately available,
     *          otherwise <code>null</code>
     */
    // 從引用佇列中獲取引用物件
    public Reference<? extends T> poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            // 執行reallyPoll之前必須獲取鎖
            return reallyPoll();
        }
    }

    /**
     * Removes the next reference object in this queue, blocking until either
     * one becomes available or the given timeout period expires.
     *
     * <p> This method does not offer real-time guarantees: It schedules the
     * timeout as if by invoking the {@link Object#wait(long)} method.
     *
     * @param  timeout  If positive, block for up to <code>timeout</code>
     *                  milliseconds while waiting for a reference to be
     *                  added to this queue.  If zero, block indefinitely.
     *
     * @return  A reference object, if one was available within the specified
     *          timeout period, otherwise <code>null</code>
     *
     * @throws  IllegalArgumentException
     *          If the value of the timeout argument is negative
     *
     * @throws  InterruptedException
     *          If the timeout wait is interrupted
     */
    // poll方法不保證一定能拿到一個引用佇列中的引用物件(如果引用佇列沒有引用物件則拿不到)
    // remove當拿的時候沒有引用佇列為空的話,會阻塞執行緒,直到佇列中新增了引用物件或者超時
    // 因此不能保證時效性
    // 這也就解釋了為什麼在enqueue方法的最後呼叫了Object.notifyAll方法
    // remove也有可能拿到空物件
    // 同時會響應中斷
    public Reference<? extends T> remove(long timeout)
        throws IllegalArgumentException, InterruptedException
    {
        if (timeout < 0) {
            throw new IllegalArgumentException("Negative timeout value");
        }
        synchronized (lock) {
            // 執行reallyPoll之前必須獲取鎖
            Reference<? extends T> r = reallyPoll();
            if (r != null) return r;
            long start = (timeout == 0) ? 0 : System.nanoTime();
            // 迴圈獲取引用佇列中head指向的引用物件
            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;
                }
            }
        }
    }

    /**
     * Removes the next reference object in this queue, blocking until one
     * becomes available.
     *
     * @return A reference object, blocking until one becomes available
     * @throws  InterruptedException  If the wait is interrupted
     */
    // 同remove方法
    public Reference<? extends T> remove() throws InterruptedException {
        return remove(0);
    }

    /**
     * Iterate queue and invoke given action with each Reference.
     * Suitable for diagnostic purposes.
     * WARNING: any use of this method should make sure to not
     * retain the referents of iterated references (in case of
     * FinalReference(s)) so that their life is not prolonged more
     * than necessary.
     */
    // 迭代整個引用佇列,並對每個引用物件執行給定的操作。
    void forEach(Consumer<? super Reference<? extends T>> action) {
        for (Reference<? extends T> r = head; r != null;) {
            action.accept(r);
            @SuppressWarnings("unchecked")
            Reference<? extends T> rn = r.next;
            if (rn == r) {
                if (r.queue == ENQUEUED) {
                    // still enqueued -> we reached end of chain
                    r = null;
                } else {
                    // already dequeued: r.queue == NULL; ->
                    // restart from head when overtaken by queue poller(s)
                    r = head;
                }
            } else {
                // next in chain
                r = rn;
            }
        }
    }
}

個人感覺不論新增緩緩i刪除元素都是從head處,叫引用棧更合適。感覺特性更像一個棧。

總結

Reference是非強引用的其他三種型別的引用的父類。而ReferenceQueue的結構指儲存了一個連結串列的頭結點,通過頭結點來完成入隊和出對的操作(感覺更像棧)。
Reference類中使用靜態程式碼塊建立了一個ReferenceHandler守護執行緒來完成將pending狀態的引用物件放到引用佇列中。本文其實還差了一個FinaReference
這個等總結垃圾回收的時候再總結。

參考資料

相關文章