java編程Reference核心原理示例源碼分析
帶著問題,看源碼針對性會更強一點、印象會更深刻、並且效果也會更好。所以我先賣個關子,提兩個問題(沒準下次跳槽時就被問到)。
- 我們可以用ByteBuffer的allocateDirect方法,申請一塊堆外內存創建一個DirectByteBuffer對象,然後利用它去操作堆外內存。這些申請完的堆外內存,我們可以回收嗎?可以的話是通過什麼樣的機制回收的?
- 大傢應該都知道WeakHashMap可以用來實現內存相對敏感的本地緩存,為什麼WeakHashMap合適這種業務場景,其內部實現會做什麼特殊處理呢?
GC可到達性與JDK中Reference類型
上面提到的兩個問題,其答案都在JDK的Reference裡面。JDK早期版本中並沒有Reference相關的類,這導致對象被GC回收後如果想做一些額外的清理工作(比如socket、堆外內存等)是無法實現的,同樣如果想要根據堆內存的實際使用情況決定要不要去清理一些內存敏感的對象也是法實現的。
為此JDK1.2中引入的Reference相關的類,即今天要介紹的Reference、SoftReference、WeakReference、PhantomReference,還有與之相關的Cleaner、ReferenceQueue、ReferenceHandler等。與Reference相關核心類基本都在java.lang.ref包下面。其類關系如下:
其中,SoftReference代表軟引用對象,垃圾回收器會根據內存需求酌情回收軟引用指向的對象。普通的GC並不會回收軟引用,隻有在即將OOM的時候(也就是最後一次Full GC)如果被引用的對象隻有SoftReference指向的引用,才會回收。WeakReference代表弱引用對象,當發生GC時,如果被引用的對象隻有WeakReference指向的引用,就會被回收。PhantomReference代表虛引用對象(也有叫幻象引用的,個人認為還是虛引用更加貼切),其是一種特殊的引用類型,不能通過虛引用獲取到其關聯的對象,但當GC時如果其引用的對象被回收,這個事件程序可以感知,這樣我們可以做相應的處理。最後就是最常見強引用對象,也就是通常我們new出來的對象。在繼續介紹Reference相關類的源碼前,先來簡單的看一下GC如何決定一個對象是否可被回收。其基本思路是從GC Root開始向下搜索,如果對象與GC Root之間存在引用鏈,則對象是可達的,GC會根據是否可到達與可到達性決定對象是否可以被回收。而對象的可達性與引用類型密切相關,對象的可到達性可分為5種。
- 強可到達,如果從GC Root搜索後,發現對象與GC Root之間存在強引用鏈則為強可到達。強引用鏈即有強引用對象,引用瞭該對象。
- 軟可到達,如果從GC Root搜索後,發現對象與GC Root之間不存在強引用鏈,但存在軟引用鏈,則為軟可到達。軟引用鏈即有軟引用對象,引用瞭該對象。
- 弱可到達,如果從GC Root搜索後,發現對象與GC Root之間不存在強引用鏈與軟引用鏈,但有弱引用鏈,則為弱可到達。弱引用鏈即有弱引用對象,引用瞭該對象。
- 虛可到達,如果從GC Root搜索後,發現對象與GC Root之間隻存在虛引用鏈則為虛可到達。虛引用鏈即有虛引用對象,引用瞭該對象。
- 不可達,如果從GC Root搜索後,找不到對象與GC Root之間的引用鏈,則為不可到達。
看一個簡單的列子:
ObjectA為強可到達,ObjectB也為強可到達,雖然ObjectB對象被SoftReference ObjcetE 引用但由於其還被ObjectA引用所以為強可到達;而ObjectC和ObjectD為弱引用達到,雖然ObjectD對象被PhantomReference ObjcetG引用但由於其還被ObjectC引用,而ObjectC又為弱引用達到,所以ObjectD為弱引用達到;而ObjectH與ObjectI是不可到達。引用鏈的強弱有關系依次是 強引用 > 軟引用 > 弱引用 > 虛引用,如果有更強的引用關系存在,那麼引用鏈到達性,將由更強的引用有關系決定。
Reference核心處理流程
JVM在GC時如果當前對象隻被Reference對象引用,JVM會根據Reference具體類型與堆內存的使用情況決定是否把對應的Reference對象加入到一個由Reference構成的pending鏈表上,如果能加入pending鏈表JVM同時會通知ReferenceHandler線程進行處理。ReferenceHandler線程是在Reference類被初始化時調用的,其是一個守護進程並且擁有最高的優先級。Reference類靜態初始化塊代碼如下:
static { //省略部分代碼... Thread handler = new ReferenceHandler(tg, "Reference Handler"); handler.setPriority(Thread.MAX_PRIORITY); handler.setDaemon(true); handler.start(); //省略部分代碼... }
而ReferenceHandler線程內部的run方法會不斷地從Reference構成的pending鏈表上獲取Reference對象,如果能獲取則根據Reference的具體類型進行不同的處理,不能則調用wait方法等待GC回收對象處理pending鏈表的通知。ReferenceHandler線程run方法源碼:
public void run() { //死循環,線程啟動後會一直運行 while (true) { tryHandlePending(true); } }
run內部調用的tryHandlePending源碼:
static boolean tryHandlePending(boolean waitForNotify) { Reference<Object> r; Cleaner c; try { synchronized (lock) { if (pending != null) { r = pending; //instanceof 可能會拋出OOME,所以在將r從pending鏈上斷開前,做這個處理 c = r instanceof Cleaner ? (Cleaner) r : null; //將將r從pending鏈上斷開 pending = r.discovered; r.discovered = null; } else { //等待CG後的通知 if (waitForNotify) { lock.wait(); } //重試 return waitForNotify; } } } catch (OutOfMemoryError x) { //當拋出OOME時,放棄CPU的運行時間,這樣有希望收回一些存活的引用並且GC能回收部分空間。同時能避免頻繁地自旋重試,導致連續的OOME異常 Thread.yield(); //重試 return true; } catch (InterruptedException x) { //重試 return true; } //如果是Cleaner類型的Reference調用其clean方法並退出 if (c != null) { c.clean(); return true; } ReferenceQueue<? super Object> q = r.queue; //如果Reference有註冊ReferenceQueue,則處理pending指向的Reference結點將其加入ReferenceQueue中 if (q != ReferenceQueue.NULL) q.enqueue(r); return true; }
上面tryHandlePending方法中比較重要的點是c.clean()與q.enqueue®,這個是文章最開始提到的兩個問題答案的入口。Cleaner的clean方法用於完成清理工作,而ReferenceQueue是將被回收對象加入到對應的Reference列隊中,等待其他線程的後繼處理。更具體地關於Cleaner與ReferenceQueue後面會再詳細說明。Reference的核心處理流程可總結如下:
對Reference的核心處理流程有整體瞭解後,再來回過頭細看一下Reference類的源碼。
/* Reference實例有四種內部的狀態 * Active: 新創建Reference的實例其狀態為Active。當GC檢測到Reference引用的referent可達到狀態發生改變時, * 為改變Reference的狀態為Pending或Inactive。這個取決於創建Reference實例時是否註冊過ReferenceQueue。 * 註冊過其狀態會轉換為Pending,同時GC會將其加入pending-Reference鏈表中,否則為轉換為Inactive狀態。 * Pending: 代表Reference是pending-Reference鏈表的成員,等待ReferenceHandler線程調用Cleaner#clean * 或ReferenceQueue#enqueue操作。未註冊過ReferenceQueue的實例不會達到這個狀態 * Enqueued: Reference實例成為其被創建時註冊過的ReferenceQueue的成員,代表已入隊列。當其從ReferenceQueue * 中移除後,其狀態會變為Inactive。 * Inactive: 什麼也不會做,一旦處理該狀態,就不可再轉換。 * 不同狀態時,Reference對應的queue與成員next變量值(next可理解為ReferenceQueue中的下個結點的引用)如下: * Active: queue為Reference實例被創建時註冊的ReferenceQueue,如果沒註冊為Null。此時,next為null, * Reference實例與queue真正產生關系。 * Pending: queue為Reference實例被創建時註冊的ReferenceQueue。next為當前實例本身。 * Enqueued: queue為ReferenceQueue.ENQUEUED代表當前實例已入隊列。next為queue中的下一實列結點, * 如果是queue尾部則為當前實例本身 * Inactive: queue為ReferenceQueue.NULL,當前實例已從queue中移除與queue無關聯。next為當前實例本身。 */ public abstract class Reference<T> { // Reference 引用的對象 private T referent; /* Reference註冊的queue用於ReferenceHandler線程入隊列處理與用戶線程取Reference處理。 * 其取值會根據Reference不同狀態發生改變,具體取值見上面的分析 */ volatile ReferenceQueue<? super T> queue; // 可理解為註冊的queue中的下一個結點的引用。其取值會根據Reference不同狀態發生改變,具體取值見上面的分析 volatile Reference next; /* 其由VM維護,取值會根據Reference不同狀態發生改變, * 狀態為active時,代表由GC維護的discovered-Reference鏈表的下個節點,如果是尾部則為當前實例本身 * 狀態為pending時,代表pending-Reference的下個節點的引用。否則為null */ transient private Reference<T> discovered; /* pending-Reference 鏈表頭指針,GC回收referent後會將Reference加pending-Reference鏈表。 * 同時ReferenceHandler線程會獲取pending指針,不為空時Cleaner.clean()或入列queue。 * pending-Reference會采用discovered引用接鏈表的下個節點。 */ private static Reference<Object> pending = null; // 可理解為註冊的queue中的下一個結點的引用。其取值會根據Reference不同狀態發生改變,具體取值見上面的分析 volatile Reference next; //用於CG同步Reference成員變量值的對象。 static private class Lock { } private static Lock lock = new Lock(); //省略部分代碼... }
上面解釋瞭Reference中的主要成員的作用,其中比較重要是Reference內部維護的不同狀態,其狀態不同成員變量queue、pending、discovered、next的取值都會發生變化。Reference的主要方法如下:
//構造函數,指定引用的對象referent Reference(T referent) { this(referent, null); } //構造函數,指定引用的對象referent與註冊的queue Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; } //獲取引用的對象referent public T get() { return this.referent; } //將當前對象加入創建時註冊的queue中 public boolean enqueue() { return this.queue.enqueue(this); }
ReferenecQueue與Cleaner源碼分析
先來看下ReferenceQueue的主要成員變量的含義。
//代表Reference的queue為null。Null為ReferenceQueue子類 static ReferenceQueue<Object> NULL = new Null<>(); //代表Reference已加入當前ReferenceQueue中。 static ReferenceQueue<Object> ENQUEUED = new Null<>(); //用於同步的對象 private Lock lock = new Lock(); //當前ReferenceQueue中的頭節點 private volatile Reference<? extends T> head = null; //ReferenceQueue的長度 private long queueLength = 0;
ReferenceQueue中比較重要的方法為enqueue、poll、remove方法。
//入列隊enqueue方法,隻被Reference類調用,也就是上面分析中ReferenceHandler線程為調用 boolean enqueue(Reference<? extends T> r) { //獲取同步對象lock對應的監視器對象 synchronized (lock) { //獲取r關聯的ReferenceQueue,如果創建r時未註冊ReferenceQueue則為NULL,同樣如果r已從ReferenceQueue中移除其也為null ReferenceQueue<?> queue = r.queue; //判斷queue是否為NULL 或者 r已加入ReferenceQueue中,是的話則入隊列失敗 if ((queue == NULL) || (queue == ENQUEUED)) { return false; } assert queue == this; //設置r的queue為已入隊列 r.queue = ENQUEUED; //如果ReferenceQueue頭節點為null則r的next節點指向當前節點,否則指向頭節點 r.next = (head == null) ? r : head; //更新ReferenceQueue頭節點 head = r; //列隊長度加1 queueLength++; //為FinalReference類型引用增加FinalRefCount數量 if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(1); } //通知remove操作隊列有節點 lock.notifyAll(); return true; } }
poll方法源碼相對簡單,其就是從ReferenceQueue的頭節點獲取Reference。
public Reference<? extends T> poll() { //頭結點為null直接返回,代表Reference還沒有加入ReferenceQueue中 if (head == null) return null; //獲取同步對象lock對應的監視器對象 synchronized (lock) { return reallyPoll(); } } //從隊列中真正poll元素的方法 private Reference<? extends T> reallyPoll() { Reference<? extends T> r = head; //double check 頭節點不為null if (r != null) { //保存頭節點的下個節點引用 Reference<? extends T> rn = r.next; //更新queue頭節點引用 head = (rn == r) ? null : rn; //更新Reference的queue值,代表r已從隊列中移除 r.queue = NULL; //更新Reference的next為其本身 r.next = r; queueLength--; //為FinalReference節點FinalRefCount數量減1 if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(-1); } //返回獲取的節點 return r; } return null; }
remove方法的源碼如下:
public Reference<? extends T> remove(long timeout) throws IllegalArgumentException, InterruptedException { if (timeout < 0) { throw new IllegalArgumentException("Negative timeout value"); } //獲取同步對象lock對應的監視器對象 synchronized (lock) { //獲取隊列頭節點指向的Reference Reference<? extends T> r = reallyPoll(); //獲取到返回 if (r != null) return r; long start = (timeout == 0) ? 0 : System.nanoTime(); //在timeout時間內嘗試重試獲取 for (;;) { //等待隊列上有結點通知 lock.wait(timeout); //獲取隊列中的頭節點指向的Reference r = reallyPoll(); //獲取到返回 if (r != null) return r; if (timeout != 0) { long end = System.nanoTime(); timeout -= (end - start) / 1000_000; //已超時但還沒有獲取到隊列中的頭節點指向的Reference返回null if (timeout <= 0) return null; start = end; } } } }
簡單的分析完ReferenceQueue的源碼後,再來整體回顧一下Reference的核心處理流程。JVM在GC時如果當前對象隻被Reference對象引用,JVM會根據Reference具體類型與堆內存的使用情況決定是否把對應的Reference對象加入到一個由Reference構成的pending鏈表上,如果能加入pending鏈表JVM同時會通知ReferenceHandler線程進行處理。ReferenceHandler線程收到通知後會調用Cleaner#clean或ReferenceQueue#enqueue方法進行處理。如果引用當前對象的Reference類型為WeakReference且堆內存不足,那麼JMV就會把WeakReference加入到pending-Reference鏈表上,然後ReferenceHandler線程收到通知後會異步地做入隊列操作。而我們的應用程序中的線程便可以不斷地去拉取ReferenceQueue中的元素來感知JMV的堆內存是否出現瞭不足的情況,最終達到根據堆內存的情況來做一些處理的操作。實際上WeakHashMap低層便是過通上述過程實現的,隻不過實現細節上有所偏差,這個後面再分析。再來看看ReferenceHandler線程收到通知後可能會調用的另外一個類Cleaner的實現。
同樣先看一下Cleaner的成員變量,再看主要的方法實現。
//繼承瞭PhantomReference類也就是虛引用,PhantomReference源碼很簡單隻是重寫瞭get方法返回null public class Cleaner extends PhantomReference<Object> { /* 虛隊列,命名很到位。之前說CG把ReferenceQueue加入pending-Reference鏈中後,ReferenceHandler線程在處理時 * 是不會將對應的Reference加入列隊的,而是調用Cleaner.clean方法。但如果Reference不註冊ReferenceQueue,GC處理時 * 又無法把他加入到pending-Reference鏈中,所以Cleaner裡面有瞭一個dummyQueue成員變量。 */ private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue(); //Cleaner鏈表的頭結點 private static Cleaner first = null; //當前Cleaner節點的後續節點 private Cleaner next = null; //當前Cleaner節點的前續節點 private Cleaner prev = null; //真正執行清理工作的Runnable對象,實際clean內部調用thunk.run()方法 private final Runnable thunk; //省略部分代碼... }
從上面的成變量分析知道Cleaner實現瞭雙向鏈表的結構。先看構造函數與clean方法。
//私有方法,不能直接new private Cleaner(Object var1, Runnable var2) { super(var1, dummyQueue); this.thunk = var2; } //創建Cleaner對象,同時加入Cleaner鏈中。 public static Cleaner create(Object var0, Runnable var1) { return var1 == null ? null : add(new Cleaner(var0, var1)); } //頭插法將新創意的Cleaner對象加入雙向鏈表,synchronized保證同步 private static synchronized Cleaner add(Cleaner var0) { if (first != null) { var0.next = first; first.prev = var0; } //更新頭節點引用 first = var0; return var0; } public void clean() { //從Cleaner鏈表中先移除當前節點 if (remove(this)) { try { //調用thunk.run()方法執行對應清理邏輯 this.thunk.run(); } catch (final Throwable var2) { //省略部分代碼.. } } }
可以看到Cleaner的實現還是比較簡單,Cleaner實現為PhantomReference類型的引用。當JVM GC時如果發現當前處理的對象隻被PhantomReference類型對象引用,同之前說的一樣其會將該Reference加pending-Reference鏈中上,隻是ReferenceHandler線程在處理時如果PhantomReference類型實際類型又是Cleaner的話。其就是調用Cleaner.clean方法做清理邏輯處理。Cleaner實際是DirectByteBuffer分配的堆外內存收回的實現,具體見下面的分析。
DirectByteBuffer堆外內存回收與WeakHashMap敏感內存回收
繞開瞭一大圈終於回到瞭文章最開始提到的兩個問題,先來看一下分配給DirectByteBuffer堆外內存是如何回收的。在創建DirectByteBuffer時我們實際是調用ByteBuffer#allocateDirect方法,而其實現如下:
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); } DirectByteBuffer(int cap) { //省略部分代碼... try { //調用unsafe分配內存 base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { //省略部分代碼... } //省略部分代碼... //前面分析中的Cleaner對象創建,持有當前DirectByteBuffer的引用 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
裡面和DirectByteBuffer堆外內存回收相關的代碼便是Cleaner.create(this, new Deallocator(base, size, cap))這部分。還記得之前說實際的清理邏輯是裡面和DirectByteBuffer堆外內存回收相關的代碼便是Cleaner裡面的Runnable#run方法嗎?直接看Deallocator.run方法源碼:
public void run() { if (address == 0) { // Paranoia return; } //通過unsafe.freeMemory釋放創建的堆外內存 unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); }
終於找到瞭分配給DirectByteBuffer堆外內存是如何回收的的答案。再總結一下,創建DirectByteBuffer對象時會創建一個Cleaner對象,Cleaner對象持有瞭DirectByteBuffer對象的引用。當JVM在GC時,如果發現DirectByteBuffer被地方法沒被引用啦,JVM會將其對應的Cleaner加入到pending-reference鏈表中,同時通知ReferenceHandler線程處理,ReferenceHandler收到通知後,會調用Cleaner#clean方法,而對於DirectByteBuffer創建的Cleaner對象其clean方法內部會調用unsafe.freeMemory釋放堆外內存。最終達到瞭DirectByteBuffer對象被GC回收其對應的堆外內存也被回收的目的。
再來看一下文章開始提到的另外一個問題WeakHashMap如何實現敏感內存的回收。實際WeakHashMap實現上其Entry繼承瞭WeakReference。
//Entry繼承瞭WeakReference, WeakReference引用的是Map的key private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; final int hash; Entry<K,V> next; /** * 創建Entry對象,上面分析過的ReferenceQueue,這個queue實際是WeakHashMap的成員變量, * 創建WeakHashMap時其便被初始化 final ReferenceQueue<Object> queue = new ReferenceQueue<>() */ 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; } //省略部分原碼... }
往WeakHashMap添加元素時,實際都會調用Entry的構造方法,也就是會創建一個WeakReference對象,這個對象的引用的是WeakHashMap剛加入的Key,而所有的WeakReference對象關聯在同一個ReferenceQueue上。我們上面說過JVM在GC時,如果發現當前對象隻有被WeakReference對象引用,那麼會把其對應的WeakReference對象加入到pending-reference鏈表上,並通知ReferenceHandler線程處理。而ReferenceHandler線程收到通知後,對於WeakReference對象會調用ReferenceQueue#enqueue方法把他加入隊列裡面。現在我們隻要關註queue裡面的元素在WeakHashMap裡面是在哪裡被拿出去啦做瞭什麼樣的操作,就能找到文章開始問題的答案啦。最終能定位到WeakHashMap的expungeStaleEntries方法。
private void expungeStaleEntries() { //不斷地從ReferenceQueue中取出,那些隻有被WeakReference對象引用的對象的Reference for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { //轉為 entry 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; //如果當前元素(也就是entry)與queue取出的一致,將entry從鏈表中去除 if (p == e) { if (prev == e) table[i] = next; else prev.next = next; // Must not null out e.next; //清空entry對應的value e.value = null; size--; break; } prev = p; p = next; } } } }
現在隻看一下WeakHashMap哪些地方會調用expungeStaleEntries方法就知道什麼時候WeakHashMap裡面的Key變得軟可達時我們就可以將其對應的Entry從WeakHashMap裡面移除。直接調用有三個地方分別是getTable方法、size方法、resize方法。 getTable方法又被很多地方調用如get、containsKey、put、remove、containsValue、replaceAll。最終看下來,隻要對WeakHashMap進行操作就行調用expungeStaleEntries方法。所有隻要操作瞭WeakHashMap,沒WeakHashMap裡面被再用到的Key對應的Entry就會被清除。再來總結一下,為什麼WeakHashMap適合作為內存敏感緩存的實現。當JVM 在GC時,如果發現WeakHashMap裡面某些Key沒地方在被引用啦(WeakReference除外),JVM會將其對應的WeakReference對象加入到pending-reference鏈表上,並通知ReferenceHandler線程處理。而ReferenceHandler線程收到通知後將對應引用Key的WeakReference對象加入到 WeakHashMap內部的ReferenceQueue中,下次再對WeakHashMap做操作時,WeakHashMap內部會清除那些沒有被引用的Key對應的Entry。這樣就達到瞭每操作WeakHashMap時,自動的檢索並清量沒有被引用的Key對應的Entry的目地。
總結
本文通過兩個問題引出瞭JDK中Reference相關類的源碼分析,最終給出瞭問題的答案。但實際上一般開發規范中都會建議禁止重寫Object#finalize方法同樣與Reference類關系密切(具體而言是Finalizer類)。受篇幅的限制本文並未給出分析,有待各位自己看源碼啦。半年沒有寫文章啦,有點對不住關註的小夥伴。希望看完本文各位或多或少能有所收獲。如果覺得本文不錯就幫忙轉發記得標一下出處,謝謝。後面我還會繼續分享一些自己覺得比較重要的東西給大傢。由於個人能力有限,文中不足與錯誤還望指正。
以上就是java編程Reference核心原理示例源碼分析的詳細內容,更多關於Reference核心原理的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 淺談JVM垃圾回收之哪些對象可以被回收
- WeakHashMap 和 HashMap 區別及使用場景
- Java WeakHashMap案例詳解
- Android中深入學習對象的四種引用類型
- Java 中的 Unsafe 魔法類的作用大全