「基礎」四種引用類型:強引用、軟引用、弱引用、虛引用


java.lang.ref整體包結構:

引用與對象

每種編程語言都有自己操作內存中元素的方式,例如在 C 和 C++ 里是通過指針,而在 Java 中則是通過“引用”。

在 Java 中一切都被視為了對象,但是我們操作的標識符實際上是對象的一個引用(reference)。

//創建一個引用,引用可以獨立存在,並不一定需要與一個對象關聯
String s;

通過將這個叫“引用”的標識符指向某個對象,之后便可以通過這個引用來實現操作對象了。

String str = new String("abc");
System.out.println(str.toString());

在 JDK1.2 之前,Java中的定義很傳統:如果 reference 類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱為這塊內存代表着一個引用。

Java 中的垃圾回收機制在判斷是否回收某個對象的時候,都需要依據“引用”這個概念。

在不同垃圾回收算法中,對引用的判斷方式有所不同:

  • 引用計數法:為每個對象添加一個引用計數器,每當有一個引用指向它時,計數器就加1,當引用失效時,計數器就減1,當計數器為0時,則認為該對象可以被回收(目前在Java中已經棄用這種方式了)。
  • 可達性分析算法:從一個被稱為 GC Roots 的對象開始向下搜索,如果一個對象到GC Roots沒有任何引用鏈相連時,則說明此對象不可用。

JDK1.2 之前,一個對象只有“已被引用”和"未被引用"兩種狀態,這將無法描述某些特殊情況下的對象,比如,當內存充足時需要保留,而內存緊張時才需要被拋棄的一類對象。

四種引用類型

在 JDK.1.2 之后,Java 對引用的概念進行了擴充,將引用分為了:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4 種,這 4 種引用的強度依次減弱。

StrongReference > SoftReference > WeakReference > PhantomReference

強引用

Java中默認聲明的就是強引用,比如:

Object obj = new Object(); //只要obj還指向Object對象,Object對象就不會被回收
obj = null;  //手動置null

只要強引用存在,垃圾回收器將永遠不會回收被引用的對象,哪怕內存不足時,JVM也會直接拋出OutOfMemoryError,不會去回收。如果想中斷強引用與對象之間的聯系,可以顯示的將強引用賦值為null,這樣一來,JVM就可以適時的回收對象了。

軟引用

軟引用是用來描述一些非必需但仍有用的對象。在內存足夠的時候,軟引用對象不會被回收,只有在內存不足時,系統則會回收軟引用對象,如果回收了軟引用對象之后仍然沒有足夠的內存,才會拋出內存溢出異常。這種特性常常被用來實現緩存技術,比如網頁緩存,圖片緩存等。

在 JDK1.2 之后,用java.lang.ref.SoftReference類來表示軟引用。

下面以一個例子來進一步說明強引用和軟引用的區別:

在運行下面的Java代碼之前,需要先配置參數 -Xms2M -Xmx3M,將 JVM 的初始內存設為2M,最大可用內存為 3M。

首先先來測試一下強引用,在限制了 JVM 內存的前提下,下面的代碼運行正常

public class TestOOM {
	
	public static void main(String[] args) {
		 testStrongReference();
	}
	private static void testStrongReference() {
		// 當 new byte為 1M 時,程序運行正常
		byte[] buff = new byte[1024 * 1024 * 1];
	}
}

但是如果我們將

byte[] buff = new byte[1024 * 1024 * 1];

替換為創建一個大小為 2M 的字節數組

byte[] buff = new byte[1024 * 1024 * 2];

則內存不夠使用,程序直接報錯,強引用並不會被回收

接着來看一下軟引用會有什么不一樣,在下面的示例中連續創建了 10 個大小為 1M 的字節數組,並賦值給了軟引用,然后循環遍歷將這些對象打印出來。

public class TestOOM {
	private static List<Object> list = new ArrayList<>();
	public static void main(String[] args) {
	     testSoftReference();
	}
	private static void testSoftReference() {
		for (int i = 0; i < 10; i++) {
			byte[] buff = new byte[1024 * 1024];
			SoftReference<byte[]> sr = new SoftReference<>(buff);
			list.add(sr);
		}
		
		System.gc(); //主動通知垃圾回收
		
		for(int i=0; i < list.size(); i++){
			Object obj = ((SoftReference) list.get(i)).get();
			System.out.println(obj);
		}
		
	}
	
}

打印結果:

我們發現無論循環創建多少個軟引用對象,打印結果總是只有最后一個對象被保留,其他的obj全都被置空回收了。

這里就說明了在內存不足的情況下,軟引用將會被自動回收。

值得注意的一點 , 即使有 byte[] buff 引用指向對象, 且 buff 是一個strong reference, 但是 SoftReference sr 指向的對象仍然被回收了,這是因為Java的編譯器發現了在之后的代碼中, buff 已經沒有被使用了, 所以自動進行了優化。

如果我們將上面示例稍微修改一下:

private static void testSoftReference() {
	byte[] buff = null;

	for (int i = 0; i < 10; i++) {
		buff = new byte[1024 * 1024];
		SoftReference<byte[]> sr = new SoftReference<>(buff);
		list.add(sr);
	}

	System.gc(); //主動通知垃圾回收
	
	for(int i=0; i < list.size(); i++){
		Object obj = ((SoftReference) list.get(i)).get();
		System.out.println(obj);
	}

	System.out.println("buff: " + buff.toString());
}

則 buff 會因為強引用的存在,而無法被垃圾回收,從而拋出OOM的錯誤。

如果一個對象唯一剩下的引用是軟引用,那么該對象是軟可及的(softly reachable)。垃圾收集器並不像其收集弱可及的對象一樣盡量地收集軟可及的對象,相反,它只在真正 “需要” 內存時才收集軟可及的對象。

弱引用

弱引用的引用強度比軟引用要更弱一些,無論內存是否足夠,只要 JVM 開始進行垃圾回收,那些被弱引用關聯的對象都會被回收

在 JDK1.2 之后,用 java.lang.ref.WeakReference來表示弱引用。

我們以與軟引用同樣的方式來測試一下弱引用:

private static void testWeakReference() {
	for (int i = 0; i < 10; i++) {
		byte[] buff = new byte[1024 * 1024];
		WeakReference<byte[]> sr = new WeakReference<>(buff);
		list.add(sr);
	}
	
	System.gc(); //主動通知垃圾回收
	
	for(int i=0; i < list.size(); i++){
		Object obj = ((WeakReference) list.get(i)).get();
		System.out.println(obj);
	}
}

打印結果:

可以發現所有被弱引用關聯的對象都被垃圾回收了。

虛引用

虛引用是最弱的一種引用關系,如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,它隨時可能會被回收,在 JDK1.2 之后,用 PhantomReference 類來表示,通過查看這個類的源碼,發現它只有一個構造函數和一個 get() 方法,而且它的 get() 方法僅僅是返回一個null,也就是說將永遠無法通過虛引用來獲取對象,虛引用必須要和 ReferenceQueue 引用隊列一起使用。

public class PhantomReference<T> extends Reference<T> {
    /**
     * Returns this reference object's referent.  Because the referent of a
     * phantom reference is always inaccessible, this method always returns
     * <code>null</code>.
     *
     * @return  <code>null</code>
     */
    public T get() {
        return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

那么傳入它的構造方法中的 ReferenceQueue 又是如何使用的呢?

引用隊列(ReferenceQueue)

引用隊列可以與軟引用、弱引用以及虛引用一起配合使用,當垃圾回收器准備回收一個對象時,如果發現它還有引用,那么就會在回收對象之前,把這個引用加入到與之關聯的引用隊列中去。程序可以通過判斷引用隊列中是否已經加入了引用,來判斷被引用的對象是否將要被垃圾回收,這樣就可以在對象被回收之前采取一些必要的措施。

與軟引用、弱引用不同,虛引用必須和引用隊列一起使用。

ReferenceQueue內部數據結構是一個鏈表,鏈表里的元素是加入進去的Reference實例,然后通過wait和notifyAll與對象鎖實現生產者和消費者,通過這種方式模擬一個隊列。

ReferenceQueue是使用wati()和notifyAll()實現生產者和消費者模式的一個具體場景。

ReferenceQueue重點源碼解析:

static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();

這兩個靜態屬性主要用於標識加入引用隊列的引用的狀態,NULL標識該引用已被當前隊列移除過,ENQUEUED標識該引用已加入當前隊列。

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
    synchronized (lock) {
        //檢查該引用是否曾從當前隊列移除過或者已經加入當前隊列了,如果有則直接返回
        ReferenceQueue<?> queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
        r.queue = ENQUEUED;//將引用關聯的隊列統一標識為ENQUEUED
        r.next = (head == null) ? r : head;//當前引用指向head
        head = r; //將head指向當前引用(鏈表新增節點采用頭插法)
        queueLength++; //更新鏈表長度
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(1); //
        }
        lock.notifyAll(); //通知消費端
        return true;
    }
}
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); //釋放當前線程鎖,等待notify通知喚醒
            r = reallyPoll();
            if (r != null) return r;
            if (timeout != 0) {   //如果超時時間不為0則校驗超時
                long end = System.nanoTime();
                timeout -= (end - start) / 1000_000;
                if (timeout <= 0) return null;  //如果剩余時間小於0則返回
                start = end;
            }
        }
    }
}

remove嘗試移除隊列中的頭部元素,如果隊列為空則一直等待直至達到指定的超時時間。

知識擴展

FinalReference

FinalReference訪問權限為package,並且只有一個子類Finalizer,同時Finalizer 是final修飾的類,所以無法繼承擴展。

與Finalizer相關聯的則是Object中的finalize()方法,在類加載的過程中,如果當前類有覆寫finalize()方法,則其對象會被標記為finalizer類,這種類型的對象被回收前會先調用其finalize()。

具體的實現機制是,在gc進行可達性分析的時候,如果當前對象是finalizer類型的對象,並且本身不可達(與GC Roots無相連接的引用),則會被加入到一個ReferenceQueue類型的隊列(F-Queue)中。而系統在初始化的過程中,會啟動一個FinalizerThread實例的守護線程(線程名Finalizer),該線程會不斷消費F-Queue中的對象,並執行其finalize()方法(runFinalizer),並且runFinalizer方法會捕獲Throwable級別的異常,也就是說finalize()方法的異常不會導致FinalizerThread運行中斷退出。對象在執行finalize()方法后,只是斷開了與Finalizer的關聯,並不意味着會立即被回收,還是要等待下一次GC,而每個對象的finalize()方法都只會執行一次,不會重復執行

finalize()方法是對象逃脫死亡命運的最后一次機會,如果在該方法中將對象本身(this關鍵字) 賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移出"即將回收的集合"。

注意:finalize()使用不當會導致內存泄漏和內存溢出,比如SocksSocketImpl之類的服務會在finalize()中加入close()操作用於釋放資源,但是如果FinalizerThread一直沒有執行的話就會導致資源一直無法釋放,從而出現內存泄漏。還有如果某對象的finalize()方法執行時間太長或者陷入死循環,將導致F-Queue一直堆積,從而造成內存溢出(oom)。

Finalizer

1、靜態內部類FinalizerThread,是一個守護線程

//消費ReferenceQueue並執行對應元素對象的finalize()方法
private static class FinalizerThread extends Thread {
    ......
    public void run() {
        ......
        final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
        running = true;
        for (;;) {
            try {
                Finalizer f = (Finalizer)queue.remove();
                f.runFinalizer(jla);
            } catch (InterruptedException x) {
            }
        }
    }
}
//初始化的時候啟動FinalizerThread(守護線程)
static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread finalizer = new FinalizerThread(tg);
    finalizer.setPriority(Thread.MAX_PRIORITY - 2);
    finalizer.setDaemon(true);
    finalizer.start();
}

2、add方法

在jvm啟動的時候就會啟動一個守護線程去消費引用隊列,並調用引用隊列指向對象的finalize()方法。

jvm在注冊finalize()方法被覆寫的對象的時候會創建一個Finalizer對象,並且將該對象加入一個雙向鏈表中:

static void register(Object finalizee) {
    new Finalizer(finalizee);
}
private Finalizer(Object finalizee) {
    super(finalizee, queue);
    add();
}
private void add() { 
    synchronized (lock) { //頭插法構建Finalizer對象的鏈表
        if (unfinalized != null) {
            this.next = unfinalized;
            unfinalized.prev = this;
        }
        unfinalized = this;
    }
}

另外還有兩個附加線程用於消費Finalizer鏈表以及隊列:

Runtime.runFinalization()會調用runFinalization()用於消費Finalizer隊列,而java.lang.Shutdown則會在jvm退出的時候(jvm關閉鈎子)調用runAllFinalizers()用於消費Finalizer鏈表。

Cleaner

Cleaner是PhantomReference的一個子類實現,提供了比finalization(收尾機制)更輕量級和健壯的實現,因為Cleaner中的清理邏輯是由Reference.ReferenceHandler 直接調用的,而且由於是虛引用的子類,它完全不會影響指向的對象的生命周期。

一個Cleaner實例記錄了一個對象的引用,以及一個包含了清理邏輯的Runnable實例。當Cleaner指向的引用被gc回收后,Reference.ReferenceHandler會不斷消費引用隊列中的元素,當元素為Cleaner類型的時候就會調用其clean()方法。

Cleaner不是用來替代finalization的,只有在清理邏輯足夠輕量和直接的時候才適合使用Cleaner,繁瑣耗時的清理邏輯將有可能導致ReferenceHandler線程阻塞從而耽誤其它的清理任務。

源碼:

public class Cleaner extends PhantomReference<Object>
{
    //一個統一的空隊列,用於虛引用構造方法,Cleaner的trunk會被直接調用不需要通過隊列
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

    //Cleaner內部為雙向鏈表,防止虛引用本身比它們引用的對象先被gc回收,此為頭節點
    static private Cleaner first = null;

    //添加節點
    private static synchronized Cleaner add(Cleaner cl) {
        if (first != null) {    //頭插法加入節點
            cl.next = first;
            first.prev = cl;
        }
        first = cl;
        return cl;
    }
    //移除節點
    private static synchronized boolean remove(Cleaner cl) {

        //指向自己說明已經被移除
        if (cl.next == cl)
            return false;

        //移除頭部節點
        if (first == cl) {
            if (cl.next != null)
                first = cl.next;
            else
                first = cl.prev;
        }
        if (cl.next != null)//下一個節點指向前一個節點
            cl.next.prev = cl.prev;
        if (cl.prev != null)//前一個節點指向下一個節點
            cl.prev.next = cl.next;

        //自己指向自己標識已被移除
        cl.next = cl;
        cl.prev = cl;
        return true;

    }

    //清理邏輯runnable實現
    private final Runnable thunk;

    ...

    //調用清理邏輯
    public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();
        } catch (final Throwable x) {
            ...
        }
    }
}

Cleaner可以用來實現對堆外內存進行管理,DirectByteBuffer就是通過Cleaner實現堆外內存回收的:

基本原理是創建Cleaner的時候會傳入堆外內存對應的引用以及清理內存相關的runnable實現,一旦該引用被回收,則會觸發Cleaner相關機制(參見上面講解)並執行傳入的runnable實現中的清理邏輯。

DirectByteBuffer(int cap) { //構造方法中創建引用對象相關聯的Cleaner對象                 
    ...
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

private static class Deallocator implements Runnable {
    ...
    public void run() { //內存回收的邏輯(具體實現參看源碼此處不展開)
    ...
    }

}     

Reference抽象類

Reference是上面列舉的幾種引用包括Cleaner的共同父類,一些引用的通用處理邏輯均在這里面實現。

引用實例的幾個狀態:

  • Active:當處於Active狀態,gc會特殊處理引用實例,一旦gc檢測到其可達性發生變化,gc就會更改其狀態。此時分兩種情況,如果該引用實例創建時有注冊引用隊列,則會進入pending狀態,否則會進入inactive狀態。新創建的引用實例為Active。
  • Pending:當前為pending-Reference列表中的一個元素,等待被ReferenceHandler線程消費並加入其注冊的引用隊列。如果該引用實例未注冊引用隊列,則永遠不會處理這個狀態。
  • Enqueued:該引用實例創建時有注冊引用隊列並且當前處於入隊列狀態,屬於該引用隊列中的一個元素。當該引用實例從其注冊引用隊列中移除后其狀態變為Inactive。如果該引用實例未注冊引用隊列,則永遠不會處理這個狀態。
  • Inactive:當處於Inactive狀態,無需任何處理,一旦變成Inactive狀態則其狀態永遠不會再發生改變。

整體遷移流程圖如下:

Reference中的幾個關鍵屬性:

//關聯的對象的引用,根據引用類型不同gc針對性處理
private T referent;       
//引用注冊的隊列,如果有注冊隊列則回收引用會加入該隊列
volatile ReferenceQueue<? super T> queue;
//上面引用隊列referenceQueue中保存引用的鏈表
/*    active:     NULL //未加入隊列前next指向null
 *    pending:    this
 *    Enqueued:   next reference in queue (or this if last)
 *    Inactive:   this
 */
Reference next;


/* When active:   由gc管理的引用發現鏈表的下一個引用
 *     pending:   pending鏈表中的下一個元素
 *   otherwise:   NULL
 */
transient private Reference<T> discovered;  /* used by VM */

/* 
 *等待入隊列的引用鏈表,gc往該鏈表加引用對象,Reference-handler線程消費該鏈表。
 * 它通過discovered連接它的元素 
 */     
private static Reference<Object> pending = null;

ReferenceHandler:

private static class ReferenceHandler extends Thread {
    ...
    public void run() {
        while (true) {
            tryHandlePending(true); //無限循環調用tryHandlePending
        }
    }
}
static {
    ... jvm啟動時以守護線程運行ReferenceHandler
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
    //注冊JavaLangRefAccess匿名實現,堆外內存管理會用到(Bits.reserveMemory)
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}
//消費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...
                //判斷是否為Cleaner實例
                c = r instanceof Cleaner ? (Cleaner) r : null;
               //將r從pending鏈表移除
                pending = r.discovered;
                r.discovered = null;
            } else {
                // The waiting on the lock may cause an OutOfMemoryError
                // because it may try to allocate exception objects.
                //如果pending沒有元素可消費則等待通知
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        //釋放cpu資源
        Thread.yield();
        // retry
        return true;
    } catch (InterruptedException x) {
        // retry
        return true;
    }

    //調用Cleaner清理邏輯(可參考前面的7,Cleaner段落)
    if (c != null) {
        c.clean();
        return true;
    }
    //如果當前引用實例有注冊引用隊列則將其加入引用隊列
    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

總結

jvm中引用有好幾種類型的實現,gc針對這幾種不同類型的引用有着不同的回收機制,同時它們也有着各自的應用場景, 比如SoftReference可以用來做高速緩存, WeakReference也可以用來做一些普通緩存(WeakHashMap), 而PhantomReference則用在一些特殊場景,比如Cleaner就是一個很好的應用場景,它可以用來回收堆外內存。與此同時,SoftReference, WeakReference, PhantomReference這幾種弱類型引用還可以與引用隊列結合使用,使得可以在關聯引用回收之后可以做一些額外處理,甚至於Finalizer(收尾機制)都可以在對象回收過程中改變對象的生命周期。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM