JAVA幾種引用及原始碼簡析
引用簡介及分類
1.簡介
在JDK1.2以前,java中的引用的定義還是比較傳統的:如果reference型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。引用指向物件的記憶體地址,物件只有被引用和沒被引用兩種狀態。
實際上,我們更希望存在這樣的一類物件:當記憶體空間還足夠的時候,這些物件能夠保留在記憶體空間中;如果當記憶體空間在進行了垃圾收集之後還是非常緊張,則可以拋棄這些物件。基於這種特性,可以滿足很多系統的快取功能的使用場景。
所以,java對引用的概念進行了擴充。
2.分類
java中將引用分為四種類型:強、軟、弱、虛。
強引用(StrongReference ) 軟引用(SoftReference ) 弱引用:(WeakReference ) 虛引用:(PhantomReference)
通常我們開發中建立的物件都是強引用,不用顯式的指明。
Reference
是上述幾種引用的父類,SoftReference
,WeakReference
,PhantomReference
都繼承了Reference
。因為強引用不需要指定,所以java中沒有StrongReference
這個類。
引用型別跟JVM的垃圾回收行為有關。
幾種引用的強度依次遞減。
3.使用場景
強引用(StrongReference ):如果一個物件具備強引用,垃圾回收器絕不會回收它。當記憶體空間不足,JVM寧願丟擲 OutOfMemoryError
軟引用(SoftReference ):對於軟引用關聯著的物件,在JVM應用即將發生記憶體溢位異常之前,將會把這些軟引用關聯的物件列進去回收物件範圍之中進行第二次回收。如果這次回收之後還是沒有足夠的記憶體,才會丟擲記憶體溢位異常。 弱引用:(WeakReference ):被弱引用關聯的物件只能生存到下一次垃圾收集發生之前,簡言之就是:一旦發生GC必定回收被弱引用關聯的物件,不管當前的記憶體是否足夠。也就是弱引用只能活到下次GC之時。 虛引用:(PhantomReference):一個物件是否關聯到虛引用,完全不會影響該物件的生命週期,也無法通過虛引用來獲取一個物件的例項( PhantomReference
Reference#get()
並且總是返回null)。為物件設定一個虛引用的唯一目的是:能在此物件被垃圾收集器回收的時候收到一個系統通知。
原始碼分析
1. Reference及ReferenceQueue
先看Reference
的成員和構造方法:
public abstract class Reference<T> {
private T referent;
volatile ReferenceQueue<? super T> queue;
volatile Reference next;
private transient Reference<T> discovered;
Reference(T referent) {
this(referent,null);
}
Reference(T referent,ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}
複製程式碼
referent
代表這個引用關聯的物件queue引用佇列。如果引用關聯的物件即將被垃圾回收器回收,那個該物件會被新增到這個佇列中。在關聯物件時可以不指定佇列,那麼 queue
的值就是ReferenceQueue.NULL
,後續在入隊過程中,檢測到當前引用擁有的時這個佇列,會直接返回false。next引用連結串列中的下一個元素。雖然引用有引用佇列,但是引用是通過這個來形成單向連結串列的,並不依賴 ReferenceQueue
,ReferenceQueue
中只儲存了這個連結串列的head
節點。discovered基於狀態表示不同連結串列中的下一個待處理的物件,主要是pending-reference列表的下一個元素,通過JVM直接呼叫賦值。
對於這些引用的處理,在java後臺會有一個專門的守護執行緒ReferenceHandler
來執行:
private static class ReferenceHandler extends Thread {
// 保證類被載入
private static void ensureClassInitialized(Class<?> clazz) {
try {
Class.forName(clazz.getName(),true,clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
}
static {
// 靜態塊中,保證類被載入
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g,String name) {
super(g,name);
}
// 重寫run()方法
public void run() {
while (true) {
tryHandlePending(true);
}
}
}
複製程式碼
ReferenceHandler
繼承Thread
,重寫了run()
方法,裡面是個死迴圈,執行tryHandlePending()
,處理下一個pending元素。
靜態塊:
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn,tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg,"Reference Handler");
// 最高優先順序的守護執行緒
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// 省略部分程式碼
.......
}
複製程式碼
當這個類被載入,就會建立一個ReferenceHandler守護執行緒,並啟動。
再回過頭分析死迴圈中的**tryHandlePending()**方法:
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
// 如果pending!=null,則後續進行處理
r = pending;
// 關於Cleaner,我也不太懂,有待研究,知道是和finalize垃圾回收有關
c = r instanceof Cleaner ? (Cleaner) r : null;
// 當前引用的下一個元素即將成為新的pending
pending = r.discovered;
// 置空當前pending的下一個引用(不需要維護這個關係了,上一步已經用完了)
r.discovered = null;
} else {
// 如果pending==null,執行緒掛起,等待被喚醒
if (waitForNotify) {
lock.wait();
}
// 從等待狀態中恢復後return,繼續執行本方法(因為是死迴圈)
return waitForNotify;
}
}
}
// 省略部分程式碼
...............
// Fast path for cleaners
if (c != null) {
c.clean();
return true;
}
// 這裡處理 pending!=null 的情況,這裡pending引用的型別不確定,但是都是將引用的關聯物件加入到關聯的佇列中
ReferenceQueue<? super Object> q = r.queue;
// q != ReferenceQueue.NULL 判斷當前引用是否關聯了引用佇列,前面講過reference的構造方法中queue可以為null,也就是不關聯引用佇列
// 如果關聯了引用佇列,才會有入隊操作,否則不進行處理。
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
複製程式碼
這個方法就是從pending連結串列中取出元素然後加入到物件的queue佇列中.。
注意到變數pending和方法tryHandlePending都是靜態的,他們是屬於類的,並不是屬於某個物件的。
pending代表當前要處理的引用,這個引用可能是SoftReference,也有可能是WeakReference ,這都沒有關係,不同的引用在這裡都是執行相同的邏輯:將引用的關聯物件加入到關聯的佇列中去。
分析**enqueue(r)**方法:
boolean enqueue(Reference<? extends T> r) {
synchronized (lock) {
ReferenceQueue<?> queue = r.queue;
// 如果queue是NULL,則是沒有關聯引用佇列,再一次進行了判斷
// 如果queue是ENQUEUED,則表明該
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
// 入隊之後引用關聯的佇列就變成了固定的ENQUEUED,呼應前一步的判斷
r.queue = ENQUEUED;
// 將當前引用新增到head元素前面(原來的head成了當前引用的next),都是連結串列基本操作
r.next = (head == null) ? r : head;
head = r;
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}
複製程式碼
入隊主要的過程:將即將加入的引用新增到連結串列的頭部,佇列長度+1.
不是說引用佇列嗎?怎麼將引用加入到了連結串列中呢?
我們注意到,Reference中有個next成員變數,如果引用關聯的物件被回收,那麼這些引用會通過剛才的enqueue()方法和next變數形成一個單向連結串列。而ReferenceQueue名為佇列,其實並不儲存這些引用,僅僅儲存了這個連結串列的頭部元素,每次有新的引用入隊,隨之改變佇列的head即可。
分析出隊:
public Reference<? extends T> poll() {
// 頭部元素為空,連結串列中不存在元素,返回null
if (head == null)
return null;
synchronized (lock) {
// 呼叫下面的方法
return reallyPoll();
}
}
private Reference<? extends T> reallyPoll() { /* Must hold lock */
Reference<? extends T> r = head;
if (r != null) {
Reference<? extends T> rn = r.next;
head = (rn == r) ? null : rn;
r.queue = NULL;
r.next = r;
queueLength--;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
return r;
}
return null;
}
複製程式碼
因為已知head節點,所以取出元素很方便,根據連結關係就可以順利找到下一個元素並將其置為新的head。
JVM在GC時如果當前物件只被Reference物件引用,JVM會根據Reference具體型別與堆記憶體的使用情況決定是否把對應的Reference物件加入到一個由Reference構成的pending連結串列上,如果能加入pending連結串列JVM同時會通知ReferenceHandler執行緒進行處理。ReferenceHandler執行緒收到通知後會呼叫Cleaner#clean或ReferenceQueue#enqueue方法進行處理。
幾個QueueList:
每個Reference都可以看成是一個節點,多個Reference通過next,discovered和pending這三個屬性進行關聯。
通過next屬性,可以構建ReferenceQueue。就是關聯物件被回收的引用形成的連結串列。
通過pending屬性,可以構建Pending List。就是即將ReferenceQueue的引用。
通過discovered屬性,可以構建Discovered List。
注意到:next與discovered是非靜態的,pending是靜態變數。
個人愚見:pending是靜態變數,PendingList的元素是全域性的,包括SoftReference 、WeakReference 等各種Reference物件,都是通過ReferenceHandler後臺執行緒來加入各自關聯佇列的。而next和discovered是屬於某個物件的,形成的連結串列的元素都是關聯到同一個ReferenceQueue的Reference物件。當多個Reference物件被建立後,物件狀態為active,此時形成DiscovedList,discovered指的是在DiscovedList中的下一個元素;當某個Reference物件的狀態不是active,而是pending時,該物件從DiscovedList中斷開,加入pendingList,discovered指的是在pendingList中的下一個元素。
小結:
引用型別 | 被垃圾收集器回收的時機 | 主要用途 | 生命週期 |
---|---|---|---|
強引用 | 直到OOM也不會被回收 | 普遍物件的狀態 | 從建立到JVM例項終止執行 |
軟引用 | 記憶體不足時會被回收 | 有用但非必須的物件快取 | 從建立到垃圾回收並且記憶體不足時 |
弱引用 | 垃圾回收時,只能活到下一次GC | 非必須的物件快取 | 從建立到下一次垃圾回收開始 |
虛引用 | 不影響物件的垃圾回收 | 關聯的物件被垃圾收集器回收時候得到一個系統通知 | - |
加入佇列(連結串列)的的元素時什麼?不是關聯物件,而是引用Reference物件!!!
Reference物件什麼時候會新增到佇列?關聯物件被回收時!!!
2.Finalizer及FinalReference
Reference的子類中有一個特殊的型別FinalReference,Finalizer是FinalReference的子類,和Object#finalize()
有關。
2.1 finalize
在所有引用型別的父類Object中含有一個finalize方法,這個跟垃圾回收有關。我們可以重寫這個方法,在方法內進行最後的資源釋放等操作。
finalize()如何影響垃圾回收呢?
在GC演算法中,有標記-清楚、標記-整理演算法,這裡不深入討論這兩個演算法的具體過程和區別,只討論他們的標記過程,標記分為兩個階段:
第一次標記:如果物件被判定為不可達物件,也就是從GC ROOTS開始搜尋,沒有一條引用鏈可達,這個物件就會被第一次標記,等待被回收。 第二次標記:將第一次標記的物件再進行判定,分析這個物件是否有必要執行**finalize()**方法,如果有必要執行,就從待回收的集合中剔除,放置在一個叫 F-Queue
的佇列之中,並且稍後由一個優先順序低的Finalizer執行緒去取該佇列的元素,"嘗試執行"元素的finalize()
方法。 是否有必要執行,有兩點:物件沒有覆蓋繼承自Object類的 finalize()
方法,如果沒有重寫這個方法,沒必要執行。物件的 finalize()
方法已經被JVM呼叫過,已經呼叫過了就不會重複執行。
2.2 Finalizer
Finalizer繼承自FinalReference和Reference,Reference的機制它都具備。在分析原始碼之前,先用兩張圖來簡述被回收的物件執行finalize()的過程:
原始碼分析:
先看一下Finalzer的成員變數:
final class Finalizer extends FinalReference<Object> { // Finalizer關聯的ReferenceQueue,其實Finalizer是一個特殊的Reference實現 private static ReferenceQueue<Object> queue = new ReferenceQueue<>(); // 等待finalization的所有Finalizer例項連結串列的頭節點,這裡稱此連結串列為unfinalized連結串列 private static Finalizer unfinalized = null; private static final Object lock = new Object(); // 中間變數,分別記錄unfinalized連結串列中當前執行元素的下一個節點和前一個節點 private Finalizer next = null, prev = null; } 複製程式碼
當垃圾物件有必要執行finalize方法時,虛擬機器會註冊一個Finalizer,並將垃圾物件關聯到此Finalizer的referent
上,註冊方法:
/* Invoked by VM */ static void register(Object finalizee) { // 垃圾物件 // 呼叫構造方法 new Finalizer(finalizee); } private Finalizer(Object finalizee) { super(finalizee,queue); add(); } 複製程式碼
Object finalizee
就是待回收的垃圾物件,register
是由虛擬機器呼叫的,使用Finalizer的構造方法,將垃圾物件和成員變數佇列queue關聯上,Finalizer物件稍後會被加入到這個佇列中。
然後再看一下FinalizerThread
執行緒:
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g,"Finalizer");
}
public void run() {
// in case of recursive call to run()
if (running)
return;
// Finalizer thread starts before System.initializeSystemClass
// is called. Wait until JavaLangAccess is available
while (!VM.isBooted()) {
// delay until VM completes initialization
try {
VM.awaitBooted();
} catch (InterruptedException x) {
// ignore and continue
}
}
// 主要的呼叫過程
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
複製程式碼
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();
}
複製程式碼
FinalizerThread重寫了Thread的run()方法,並且是在靜態塊中初始化並啟動的。
前面的程式碼可以不看,直接分析下面的死迴圈:
Finalizer f = (Finalizer)queue.remove()
作用是從關聯的佇列中取出Finalizer元素,然後執行Finalizer的runFinalizer
方法。
runFinalizer()方法:
private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
//先獲取到垃圾物件
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
// 真正呼叫物件的finalize()方法了
jla.invokeFinalize(finalizee);
/* Clear stack slot containing this variable,to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
複製程式碼
先判斷了物件的finalize()是否已經執行過了,執行過了的不會再執行。
Object finalizee = this.get()
獲取到關聯的垃圾物件,jla.invokeFinalize(finalizee)
這行程式碼就是去實際執行垃圾物件的finalize()
方法。
小結:
類載入時會初始化 FinalizerThread
執行緒,這個執行緒任務就是從佇列中取出Finalizer物件,並執行其關聯垃圾物件的finalize()當一個物件被標記並且有必要執行finalize(),那麼虛擬機器會註冊一個Finalizer物件,關聯垃圾物件和佇列,Finalizer物件會被ReferenceHandler執行緒新增到這個佇列中。 涉及到兩個執行緒,但他們的作用不同。ReferenceHandler負責將pending連結串列中的元素新增到對應的佇列中,不關心怎麼處理佇列中元素;FinalizerThread負責從佇列中取出元素進行處理,不關心元素怎麼入隊的。
例項分析
1. 例項分析
public class ReferenceMain {
private static ReferenceQueue<ReferenceObserveObject> referenceQueue = new ReferenceQueue<>();
public static void main(String[] args) throws InterruptedException {
ReferenceObserveObject o1 = new ReferenceObserveObject(1);
ReferenceObserveObject o2 = new ReferenceObserveObject(2);
ReferenceObserveObject o3 = new ReferenceObserveObject(3);
WeakReference<ReferenceObserveObject> weakReference1 = new WeakReference<>(o1,referenceQueue);
WeakReference<ReferenceObserveObject> weakReference2 = new WeakReference<>(o2,referenceQueue);
WeakReference<ReferenceObserveObject> weakReference3 = new WeakReference<>(o3,referenceQueue);
System.out.println(weakReference1);
System.out.println(weakReference2);
System.out.println(weakReference3);
System.out.println();
// 將關聯的物件置為null,help GC
o1 = null;
o2 = null;
o3 = null;
Thread.sleep(1000);
// 通知GC回收
System.gc();
Reference<? extends ReferenceObserveObject> r;
while (true) {
r = referenceQueue.poll();
// 獲取引用關聯的物件
if( null != r){
ReferenceObserveObject roo = r.get();
System.out.println(r);
System.out.println(roo);
}
}
}
@Data
@AllArgsConstructor
private static class ReferenceObserveObject {
private int id;
@Override
public String toString() {
return "ReferenceObserveObject{" +
"id=" + id +
'}';
}
}
}
複製程式碼
結果:
java.lang.ref.WeakReference@65b54208 java.lang.ref.WeakReference@1be6f5c3 java.lang.ref.WeakReference@6b884d57 java.lang.ref.WeakReference@6b884d57 null java.lang.ref.WeakReference@65b54208 null java.lang.ref.WeakReference@1be6f5c3 null 複製程式碼
從佇列中取出的WeakReference引用與我們建立的一致,並且從中取出的關聯物件都是NULL(已被回收)。
2.WeakHashMap
WeakHashMap經常用來快取key-value形式的資料,用到了WeakReference。
WeakHashMap.Entry:
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
Entry(Object key,V value, ReferenceQueue<Object> queue, int hash,Entry<K,V> next) {
super(key,queue);
this.value = value;
this.hash = hash;
this.next = next;
}
}
複製程式碼
繼承了super(key,queue)
,每建立一個Entry例項,就相當於建立了一個WeakReference
物件,key物件就是關聯的物件,當key指向的物件被回收時,這個Entry物件就會被加入全域性的private final ReferenceQueue<Object> queue = new ReferenceQueue<>()
佇列中。
加入到佇列的是Entry物件
搜尋一下就知道queue 用在expungeStaleEntries()方法中:
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
// 從佇列中取出Entry例項,也就是這個Entry關聯的key物件被GC回收了
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash,table.length);
//
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
複製程式碼
ReferenceQueue中的Entry物件會被逐一取出,找到他們在table中的下標,然後刪除。
本文使用 mdnice 排版