1. 程式人生 > >(原始碼)詳細分析Android中的引用機制Reference(WeakReference、SoftReference、PhantomReference)

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


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部分並未涉及,以後有機會我們用新的一章來總結。另外對於強引用沒有做詳細分析,包括相互引用的回收等情況,同樣我會找個時間整理一下。謝謝大家!