(原始碼)詳細分析Android中的引用機制Reference(WeakReference、SoftReference、PhantomReference)
阿新 • • 發佈:2019-01-07
1、前言
在java中,我們知道一般情況下當一個物件被其他物件引用時,該物件則不會被回收。但是有時我們雖然需要使用該物件,但又希望不影響回收。 比如在Activity中以內部類的方式建立了一個Handler,這個Handler就會隱式的持有一個activity的引用,當這個Handler被一個耗時執行緒所引用。這時如果關閉這個Activity,由於被引用該Activity及它所持有的引用佔用的記憶體將不能被銷燬,這樣就導致了記憶體洩漏。 這時候我們可以使用“弱引用”來解決問題。2、四種引用
除了之前提到的“弱引用”,在java中還有另外三種引用,下面我們簡單談談這四種引用:- 強引用:就是程式碼中普遍存在的引用,一般情況下只要存在強引用就不會被回收。(這裡要注意相互引用的情況,我們會在另外一篇來說
- 軟引用(SoftReference):只有軟引用關聯的物件,當記憶體不足時會被回收(細節下面會說)
- 弱引用(WeakReference):在廣義上除了強引用都是弱引用,這裡我們說的是狹義上的弱引用。弱引用比軟引用還要更容易被回收,當GC過程中發現只有弱引用的物件時,不論記憶體是否足夠都會被回收。
- 虛引用(PhantomReference):虛引用對物件的生存不產生任何影響,而且通過虛引用無法獲取物件例項。虛引用的作用是我們可以通過它來判斷物件是否已經被回收,細節我們下面再聊。
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函式會被呼叫,而在這個函式中呼叫了ReferenceQueue的enqueue函式。那麼我們來看看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的時間。
- free_heap是JVM Heap的空閒大小,單位是MB
- ms_per_mb單位是毫秒,是每MB空閒允許保留軟引用的時間。Sun JVM可以通過引數-XX:SoftRefLRUPolicyMSPerMB進行設定
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物件,同樣也可以判斷。