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函數會被調用,而在這個函數中調用了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的時間。
那麼這兩個參數有什麼用?
我們知道軟引用是當內存不足時可以回收的。但是這只是大致情況,實際上軟應用的回收有一個條件:
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部分並未涉及,以後有機會我們用新的一章來總結。另外對於強引用沒有做詳細分析,包括相互引用的回收等情況,同樣我會找個時間整理一下。謝謝大家!