Java四種引用詳解

前言

為了滿足對不同情況的垃圾回收需求,從Java從版本1.2開始,引入了4種引用類型(其實是額外增加了三種)的概念。本文將詳細介紹這四種引用。

Java 4種引用類型

Java中的4中引用類型分別為強引用(String Reference)軟引用(Soft Reference),弱引用(Weak Reference)虛引用(Phantom Reference)。

概念及應(yīng)用場景

  • 強引用:Java中的引用,默認都是強引用。比如new一個對象,對它的引用就是強引用。對于被強引用指向的對象,就算JVM內(nèi)存不足OOM,也不會去回收它們。
  • 軟引用:若一個對象只被軟引用所引用,那么它將在JVM內(nèi)存不足的時候被回收,即如果JVM內(nèi)存足夠,則軟引用所指向的對象不會被垃圾回收(其實這個說法也不夠準確,具體原因后面再說)。根據(jù)這個性質(zhì),軟引用很適合做內(nèi)存緩存:既能提高查詢效率,也不會造成內(nèi)存泄漏。
  • 弱引用:若一個對象只被弱引用所引用,那么它將在下一次GC中被回收掉。如ThreadLocalWeakHashMap中都使用了弱引用,防止內(nèi)存泄漏。
  • 虛引用:虛引用是四種引用中最弱的一種引用。我們永遠無法從虛引用中拿到對象,被虛引用引用的對象就跟不存在一樣。虛引用一般用來跟蹤垃圾回收情況,或者可以完成垃圾收集器之外的一些定制化操作。Java NIO中的堆外內(nèi)存(DirectByteBuffer)因為不受GC的管理,這些內(nèi)存的清理就是通過虛引用來完成的。

引用隊列

引用隊列(Reference Queue)是一個鏈表,顧名思義,存放的是引用對象(Reference對象)的隊列。
軟引用弱引用可以和一個引用隊列(Reference Queue)配合使用,當引用所指向的對象被垃圾回收之后,該引用對象本身會被添加到與之關(guān)聯(lián)的引用隊列中,從而方便后續(xù)一些跟蹤或者額外的清理操作。
因為無法從虛引用中拿到目標對象,虛引用必須和一個引用隊列(Reference Queue)配合使用。

案例解析

設(shè)置JVM的啟動參數(shù)為

-Xms10m -Xmx10m

public class ReferenceTest {
    private static int _1MB = 1024 * 1024;
    private static int _1KB = 1024;

    public static void main(String[] args) throws InterruptedException {
        // 引用隊列,存放Reference對象
        ReferenceQueue queue = new ReferenceQueue();
        // 定義四種引用對象,強/弱/虛引用為1kb,軟引用為1mb
        Byte[] strong = new Byte[_1KB];
        SoftReference<Byte[]> soft = new SoftReference<>(new Byte[_1MB], queue);
        WeakReference<Byte[]> weak = new WeakReference<>(new Byte[_1KB], queue);
        PhantomReference<Byte[]> phantom = new PhantomReference<>(new Byte[_1KB], queue);

        Reference<String> collectedReference;
        // 初始狀態(tài)
        System.out.println("Init: Strong Reference is " + strong);
        System.out.println("Init: Soft Reference is " + soft.get());
        System.out.println("Init: Weak Reference is " + weak.get());
        System.out.println("Init: Phantom Reference is " + phantom.get());
        do {
            collectedReference = queue.poll();
            System.out.println("Init: Reference In Queue is " + collectedReference);
        }
        while (collectedReference != null);
        System.out.println("********************");

        // 第一次手動觸發(fā)GC
        System.gc();
        // 停100ms保證垃圾回收已經(jīng)執(zhí)行
        Thread.sleep(100);

        System.out.println("After GC: Strong Reference is " + strong);
        System.out.println("After GC: Soft Reference is " + soft.get());
        System.out.println("After GC: Weak Reference is " + weak.get());
        System.out.println("After GC: Phantom Reference is " + phantom.get());
        do {
            collectedReference = queue.poll();
            System.out.println("After GC: Reference In Queue is " + collectedReference);
        }
        while (collectedReference != null);
        System.out.println("********************");

        // 再分配1M的內(nèi)存,以模擬OOM的情況
        Byte[] newByte = new Byte[_1MB];

        System.out.println("After OOM: Strong Reference is " + strong);
        System.out.println("After OOM: Soft Reference is " + soft.get());
        System.out.println("After OOM: Weak Reference is " + weak.get());
        System.out.println("After OOM: Phantom Reference is " + phantom.get());
        do {
            collectedReference = queue.poll();
            System.out.println("After OOM: Reference In Queue is " + collectedReference);
        }
        while (collectedReference != null);
    }
}

上述代碼的輸出結(jié)果為:

Init: Strong Reference is [Ljava.lang.Byte;@74a14482
Init: Soft Reference is [Ljava.lang.Byte;@1540e19d
Init: Weak Reference is [Ljava.lang.Byte;@677327b6
Init: Phantom Reference is null
Init: Reference In Queue is null
********************
After GC: Strong Reference is [Ljava.lang.Byte;@74a14482
After GC: Soft Reference is [Ljava.lang.Byte;@1540e19d
After GC: Weak Reference is null
After GC: Phantom Reference is null
After GC: Reference In Queue is java.lang.ref.WeakReference@14ae5a5
After GC: Reference In Queue is java.lang.ref.PhantomReference@7f31245a
After GC: Reference In Queue is null
********************
After OOM: Strong Reference is [Ljava.lang.Byte;@74a14482
After OOM: Soft Reference is null
After OOM: Weak Reference is null
After OOM: Phantom Reference is null
After OOM: Reference In Queue is java.lang.ref.SoftReference@6d6f6e28
After OOM: Reference In Queue is null
  1. 初始狀態(tài)下,虛引用用就返回null,其他三個引用都有值。
  2. 當觸發(fā)GC之后,弱引用指向的對象也被回收了,而且可以看到弱引用虛引用兩個引用對象被加到了它們相關(guān)聯(lián)的引用隊列中了;強引用軟引用還是可以取到值。
  3. 當JVM內(nèi)存不足之后,軟引用也被內(nèi)存回收了,同時該軟引用也被加到了與之關(guān)聯(lián)的引用隊列中了。而強引用依然能取到值。

源碼解析

以下是引用類的UML圖

Reference UML

弱引用,軟引用虛引用都繼承自Reference類,我們從Reference類看起

Reference類

// 此Reference對象可能會有四種狀態(tài):active, pending, enqueued, inactive
// avtive: 新創(chuàng)建的對象狀態(tài)是active
// pending: 當Reference所指向的對象不可達,并且Reference與一個引用隊列關(guān)聯(lián),那么垃圾收集器
//     會將Reference標記為pending,并且會將之加到pending隊列里面
// enqueued: 當Reference從pending隊列中,移到引用隊列中之后,就是enqueued狀態(tài)
// inactive: 如果Reference所指向的對象不可達,并且Reference沒有與引用隊列關(guān)聯(lián),Reference
//     從引用隊列移除之后,變?yōu)閕nactive狀態(tài)。inactive就是最終狀態(tài)
public abstract class Reference<T> {
    // 該對象就是Reference所指向的對象,垃圾收集器會對此對象做特殊處理。
    private T referent;         /* Treated specially by GC */
    // Reference相關(guān)聯(lián)的引用隊列
    volatile ReferenceQueue<? super T> queue;
    // 當Reference是active時,next為null
    // 當該Reference處于引用隊列中時,next指向隊列中的下一個Reference
    // 其他情況next指向this,即自己
    // 垃圾收集器只需判斷next是不是為null,來看是否需要對此Reference做特殊處理
    volatile Reference next;
    // 當Reference在pending隊列中時,該值指向下一個隊列中Reference對象
    // 另外垃圾收集器在GC過程中,也會用此對象做標記
    transient private Reference<T> discovered;  /* used by VM */

    // 鎖對象
    static private class Lock { }
    private static Lock lock = new Lock();

    // pending隊列,這里的pending是pending鏈表的隊首元素,一般與上面的discovered變量一起使用
    private static Reference<Object> pending = null;
    // 獲取Reference指向的對象。默認返回referent對象
    public T get() {
        return this.referent;
    }
}

Reference類跟垃圾收集器緊密關(guān)聯(lián),其狀態(tài)變化如下圖所示:

Reference State

上述步驟大多數(shù)都是由GC線程來完成,其中PendingEnqueued是用戶線程來做的。Reference類中定義了一個子類ReferenceHandler,專門用來處理Pending狀態(tài)的Reference。我們來看看它具體做了什么。

ReferenceHandler類

public abstract class Reference<T> {
    // 靜態(tài)塊,主要邏輯是啟動ReferenceHandler線程
    static {
        // 創(chuàng)建ReferenceHandler線程
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent());
            Thread handler = new ReferenceHandler(tg, "Reference Handler");
        // 設(shè)置成守護線程,最高優(yōu)先級,并啟動
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
        // 訪問控制
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

    // 內(nèi)部類ReferenceHandler,用來處理Pending狀態(tài)的Reference
    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);
            }
        }
        // 靜態(tài)塊,確保InterruptedException和Cleaner已經(jīng)被ClassLoader加載
        // 因為后面會用到這兩個類
        static {
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

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

        public void run() {
            // 死循環(huán)調(diào)用tryHandlePending方法
            while (true) {
                tryHandlePending(true);
            }
        }
    }
}

Reference類在加載進JVM的時候,會啟動ReferenceHandler線程,并將它設(shè)成最高優(yōu)先級的守護線程,不斷循環(huán)調(diào)用tryHandlePending方法。
接下來看tryHandlePending方法:

    // waitForNotify默認是true。
    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            // 需要在同步塊中進行
            synchronized (lock) {
                // 判斷pending隊列是否為空,pending是隊首元素
                if (pending != null) {
                    // 取到pending隊列隊首元素,賦值給r
                    r = pending;
                    // Cleaner類是Java NIO中專門用來清理堆外內(nèi)存(DirectByteBufer)的類,這里對它做了特殊處理
                    // 當沒有其他引用指向堆外內(nèi)存時,與之關(guān)聯(lián)的Cleaner會被加到pending隊列中
                    // 如果該Reference是Cleaner實例,那么取到該Cleaner,后續(xù)可以做一些清理操作。
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // r.discovered就是下一個元素
                    // 以下操作即為將隊首元素從pending隊列移除
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // 如果pending隊列為空,則釋放鎖等待
                    // 當有Reference添加到pending隊列中時,ReferenceHandler線程會從此處被喚醒
                    if (waitForNotify) {
                        lock.wait();
                    }
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // OOM時,讓出cpu
            Thread.yield();
            return true;
        } catch (InterruptedException x) {
            return true;
        }
        // 給Cleaner的特殊處理,調(diào)用clean()方法,以釋放與之關(guān)聯(lián)的堆外內(nèi)存
        if (c != null) {
            c.clean();
            return true;
        }
        // 此處,將此Reference加入到與之關(guān)聯(lián)的引用隊列
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

看到這里,豁然開朗。ReferenceHandler線程專門用來處理pending狀態(tài)的Reference,跟GC線程組成類似生產(chǎn)者消費者的關(guān)系。當pending隊列為空,則等待;當Reference關(guān)聯(lián)的對象被回收,Reference被加入到pending隊列中之后,ReferenceHandler線程會被喚醒來處理pendingReference,主要做三件事:

  1. 將該Referencepending隊列移除
  2. 如果該ReferenceCleaner的實例,那么調(diào)用clean方法,釋放堆外內(nèi)存
  3. Reference加入到與之關(guān)聯(lián)的引用隊列

ReferenceQueue

引用隊列比較簡單,可以直接理解為一個存放Reference的鏈表,在此不再費筆墨。

虛引用PhantomReference

// 灰常簡單,只重寫了一個構(gòu)造方法,一個get方法
public class PhantomReference<T> extends Reference<T> {
    // get方法永遠返回null
    public T get() {
        return null;
    }

    // 只提供了一個包含ReferenceQueue的構(gòu)造方法,說明它必須和引用隊列一起使用
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

一般情況下虛引用使用得比較少,最為人所熟知的就是PhantomReference的子類Cleaner了,它用來清理NIO中的堆外內(nèi)存。有機會可以專門寫篇文章來講講它。

弱引用WeakReference

// 更加簡單,只重寫了兩個構(gòu)造方法
public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

太過簡單,不做額外講解。

軟引用SoftReference

// 相比WeakReference,它增加了兩個時間戳,clock和timestamp
// 這兩個參數(shù)是實現(xiàn)他們內(nèi)存回收上區(qū)別的關(guān)鍵
public class SoftReference<T> extends Reference<T> {
    // 每次GC之后,若該引用指向的對象沒有被回收,則垃圾收集器會將clock更新成當前時間
    static private long clock;
    // 每次調(diào)用get方法的時候,會更新該時間戳為clock值
    // 所以該值保存的是上一次(最近一次)GC的時間戳
    private long timestamp;

    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }

    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }
    // 每次調(diào)用,更新timestamp的值,使之等于clock的值,即最近一次gc的時間
    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}

SoftReference除了多了兩個時間戳之外,跟WeakReference幾乎沒有區(qū)別,它是如何做到在內(nèi)存不足時被回收這件事的呢?其實這是垃圾收集器干的活。垃圾收集器回收SoftReference所指向的對象,會看兩個維度:

  1. SoftReference.timestamp有多老(距上一次GC過了多久)
  2. JVM的堆空閑空間有多大

而具體什么時候回收SoftReference所指向的對象呢,可以參考如下公式:

interval <= free_heap * ms_per_mb

其中interval為上一次GC與當前時間的差值,以毫秒為單位;free_heap為當前JVM中剩余的堆空間大小,以MB為單位;ms_per_mb可以理解為一個常數(shù),即每兆空閑空間可維持的SoftReference的對象生存的時長,默認為1000,可以通過JVM參數(shù)-XX:SoftRefLRUPolicyMSPerMB設(shè)置。
如果上述表達式返回false,則清理SoftReference所指向的對象,并將該SoftReference加入到pending隊列中;否則不做處理。所以說在JVM內(nèi)存不足的時候回收軟引用這個說法不是非常準確,只是個經(jīng)驗說法,軟引用的回收,還跟它存活的時間有關(guān),甚至跟JVM參數(shù)設(shè)置(-XX:SoftRefLRUPolicyMSPerMB)都有關(guān)系!

參考

How Hotspot Clear Softreference

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容