(源碼)詳細分析Android中的引用機制Reference(WeakReference、SoftReference、PhantomReference)

Android進階之路系列:http://blog.csdn.net/column/details/16488.html



1、前言

在java中,我們知道一般情況下當一個對象被其他對象引用時,該對象則不會被回收。但是有時我們雖然需要使用該對象,但又希望不影響回收。
比如在Activity中以內部類的方式創建了一個Handler,這個Handler就會隱式的持有一個activity的引用,當這個Handler被一個耗時線程所引用。這時如果關閉這個Activity,由於被引用該Activity及它所持有的引用佔用的內存將不能被銷燬,這樣就導致了內存泄漏。
這時候我們可以使用“弱引用”來解決問題。

2、四種引用

除了之前提到的“弱引用”,在java中還有另外三種引用,下面我們簡單談談這四種引用:
  • 強引用:就是代碼中普遍存在的引用,一般情況下只要存在強引用就不會被回收。(這裏要注意相互引用的情況,我們會在另外一篇來說
  • 軟引用(SoftReference):只有軟引用關聯的對象,當內存不足時會被回收(細節下面會說)
  • 弱引用(WeakReference):在廣義上除了強引用都是弱引用,這裏我們說的是狹義上的弱引用。弱引用比軟引用還要更容易被回收,當GC過程中發現只有弱引用的對象時,不論內存是否足夠都會被回收。
  • 虛引用(PhantomReference):虛引用對對象的生存不產生任何影響,而且通過虛引用無法獲取對象實例。虛引用的作用是我們可以通過它來判斷對象是否已經被回收,細節我們下面再聊。
以上就是java中四種引用,至於他們的使用方法都比較簡單,大家可以自行搜索文章。

3、java.lang.ref

前面提到的幾種引用都在java.lang.ref包下,該包下的類如圖:

注意這是Android-26下的對應包,而不是jdk下的包,android系統對jdk的一部分類有一些改動,所以源碼有所不同。jdk下該包的類如圖:

可以看到jdk下多了Finalizer和FinalReference這兩個類。其中FinalReference是Reference的子類,而Finalizer則是FinalReference的子類。

本章我們討論Android系統下的引用,java引用我們以後另開一章來討論。
其中SoftReference、WeakReference、PhantomReference都是Reference的子類,而ReferenceQueue則是Reference的一個重要的組成部分。
下面我們來看看這幾個類的源碼。

4、Reference

源碼如下:
public abstract class Reference<T> {
    private static boolean disableIntrinsic = false;
    private static boolean slowPathEnabled = false;

    volatile T referent;        /* Treated specially by GC */
    final ReferenceQueue<? super T> queue;

    Reference queueNext;
    Reference<?> pendingNext;

    public T get() {
        return getReferent();
    }

    @FastNative
    private final native T getReferent();

    public void clear() {
        clearReferent();
    }

    @FastNative
    native void clearReferent();

    public boolean isEnqueued() {
        return queue != null && queue.isEnqueued(this);
    }

    public boolean enqueue() {
      return queue != null && queue.enqueue(this);
    }

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

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


代碼只有二三十行,我們看到Reference除了帶有對象引用referent的構造函數,還有一個帶有ReferenceQueue參數的構造函數。那麼這個ReferenceQueue用來做什麼呢?需要我們從enqueue這個函數來開始分析。當系統要回收Reference持有的對象引用referent的時候,Reference的enqueue函數會被調用,而在這個函數中調用了ReferenceQueueenqueue函數。那麼我們來看看ReferenceQueue的enqueue函數做了什麼?

5、ReferenceQueue.enqueue(Reference)

源碼如下:
boolean enqueue(Reference<? extends T> reference) {
    synchronized (lock) {
        if (enqueueLocked(reference)) {
            lock.notifyAll();
            return true;
        }
        return false;
    }
}


可以看到首先獲取同步鎖,然後調用了enqueueLocked(Reference)函數,該函數源碼如下:
private boolean enqueueLocked(Reference<? extends T> r) {
    // Verify the reference has not already been enqueued.
    if (r.queueNext != null) {
        return false;
    }

    if (r instanceof Cleaner) {
        Cleaner cl = (sun.misc.Cleaner) r;
        cl.clean();
        r.queueNext = sQueueNextUnenqueued;
        return true;
    }

    if (tail == null) {
        head = r;
    } else {
        tail.queueNext = r;
    }
    tail = r;
    tail.queueNext = r;
    return true;
}


通過 enqueueLocked函數可以看到ReferenceQueue維護了一個隊列(鏈表結構),而enqueue這一系列函數就是將reference添加到這個隊列(鏈表)中。

6、ReferenceQueue.isEnqueued()

讓我們回到Reference源碼中,可以看到除了enqueue這個函數還有一個isEnqueued函數,同樣這個函數調用了ReferenceQueue的同名函數,源碼如下:
boolean isEnqueued(Reference<? extends T> reference) {
    synchronized (lock) {
        return reference.queueNext != null && reference.queueNext != sQueueNextUnenqueued;
    }
}


可以看到先獲取同步鎖,然後判斷該reference是否在隊列(鏈表)中。由於enqueue和isEnqueue函數都要申請同步鎖,所以這是線程安全的。

這裏要注意“reference.queueNext != sQueueNextUnenqueued”用於判斷該Reference是否是一個Cleaner類,在上面ReferenceQueue的enqueueLocked函數中我們可以看到如果一個Reference是一個Cleaner,則調用它的clean方法,同時並不加入鏈表,並且將其queueNext設置爲sQueueNextUnequeued,這是一個空的虛引用,如下:
private static final Reference sQueueNextUnenqueued = new PhantomReference(null, null);


那麼什麼是Cleaner?引用一段描述
sun.misc.Cleaner是JDK內部提供的用來釋放非堆內存資源的API。JVM只會幫我們自動釋放堆內存資源,但是它提供了回調機制,通過這個類能方便的釋放系統的其他資源。
可以看到Cleaner是用於釋放非堆內存的,所以做特殊處理。

通過enqueue和isEnqueue兩個函數的分析,ReferenceQueue隊列維護了那些被回收對象referent的Reference的引用,這樣通過isEnqueue就可以判斷對象referent是否已經被回收,用於一些情況的處理

7、SoftReference

軟引用源碼如下:
public class SoftReference<T> extends Reference<T> {

    static private long clock;
    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;
    }

    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }

}


可以看到SoftReference有一個類變量clock和一個變量timestamp,這兩個參數對於SoftReference至關重要。
  • clock:記錄了上一次GC的時間。這個變量由GC(garbage collector)來改變。
  • timestamp:記錄對象被訪問(get函數)時最近一次GC的時間。
那麼這兩個參數有什麼用?
我們知道軟引用是當內存不足時可以回收的。但是這只是大致情況,實際上軟應用的回收有一個條件:
clock -timestamp <= free_heap * ms_per_mb
  • free_heap是JVM Heap的空閒大小,單位是MB
  • ms_per_mb單位是毫秒,是每MB空閒允許保留軟引用的時間。Sun JVM可以通過參數-XX:SoftRefLRUPolicyMSPerMB進行設置
舉個栗子:
目前有3MB的空閒,ms_per_mb爲1000,這時如果clock和timestamp分別爲5000和2000,那麼
5000 - 2000 <= 3 * 1000
條件成立,則該次GC不對該軟引用進行回收。
所以每次GC時,通過上面的條件去判斷軟應用是否可以回收並進行回收,即我們通常說的內存不足時被回收。

8、WeakReference

弱引用的源碼很簡單,如下:
public class WeakReference<T> extends Reference<T> {

    public WeakReference(T referent) {
        super(referent);
    }

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

}

沒有其他代碼,GC時被回收掉。

9、PhantomReference

虛引用的源碼也比較簡單,如下:
public class PhantomReference<T> extends Reference<T> {

    public T get() {
        return null;
    }

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

}


可以看到get函數返回null,正如前面說得虛引用無法獲取對象引用。(注意網上有些文章說虛引用不持有對象的引用,這是有誤的,通過構造函數可以看到虛引用是持有對象引用的,但是無法獲取該引用)
同時可以看到虛引用只有一個構造函數,所以必須傳入ReferenceQueue對象。
前面提到虛引用的作用是判斷對象是否被回收,這個功能正是通過ReferenceQueue實現的(文章第5、6點講的)。
這裏注意:不僅僅是虛引用可以判斷回收,弱引用和軟引用同樣實現了帶有ReferenceQueue的構造函數,如果創建時傳入了一個ReferenceQueue對象,同樣也可以判斷。

10、總結

本篇文章主要分析了Reference及其子類的源碼,其中Reference和ReferenceQueue只分析了部分重點代碼,其他代碼的作用大家可以自己研究一下。本次的源碼分析只涉及到java層,至於底層GC部分並未涉及,以後有機會我們用新的一章來總結。另外對於強引用沒有做詳細分析,包括相互引用的回收等情況,同樣我會找個時間整理一下。謝謝大家!



發佈了106 篇原創文章 · 獲贊 30 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章