Java 堆外記憶體回收原理
滌生的部落格
轉載請註明原創出處,謝謝
如果讀完覺得有收穫的話,歡迎點贊加關注
堆外記憶體簡介
DirectByteBuffer 這個類是 JDK 提供使用堆外記憶體的一種途徑,當然常見的業務開發一般不會接觸到,即使涉及到也可能是框架(如 Netty、RPC 等)使用的,對框架使用者來說也是透明的。
堆外記憶體優勢
堆外記憶體優勢在 IO 操作上,對於網路 IO,使用 Socket 傳送資料時,能夠節省堆記憶體到堆外記憶體的資料拷貝,所以效能更高。看過 Netty 原始碼的同學應該瞭解,Netty 使用堆外記憶體來實現零拷貝技術。對於磁碟 IO 時,也可以使用記憶體對映,來提升效能。另外,更重要的幾乎不用考慮堆記憶體煩人的 GC 問題。
堆外記憶體建立
我們直接來看程式碼,首先向 Bits 類申請額度,Bits 類內部維護著當前已經使用的堆外記憶體值,會 check 當前申請的大小與已經使用的記憶體大小是否超過總的堆外記憶體大小(預設大小與堆記憶體差不多,其實是有細微區別的,拿 CMS GC 來舉例,它的大小是新生代的最大值 - 一個 survivor 的大小 + 老生代的最大值),可以使用 -XX:MaxDirectMemorySize 引數指定堆外記憶體最大大小。
如果 check 不通過,會主動執行 System.gc(),然後 sleep 100 毫秒,再進行 check,如果記憶體還是不足,就丟擲 OOM Error。
如果 check 通過,就會呼叫 unsafe.allocateMemory 真正分配記憶體,返回記憶體地址,然後再將記憶體清 0。題外話,這個 unsafe 命名看著是不是很嚇人,這個 unsafe 不是說不安全,而是 JDK 內部使用的類,不推薦外部使用,所以叫 unsafe,Netty 原始碼內部也有類似命名。
由於申請記憶體前可能會呼叫 System.gc(),所以謹慎設定 -XX:+DisableExplicitGC 這個選項,這個引數作用是禁止程式碼中顯示觸發的 Full GC。
堆外記憶體回收
看到這段程式碼從成員的命名上就應該知道,是用來回收堆外記憶體的。確實,但是它是如何工作的呢?接下來我們看看 Cleaner 類。
Cleaner 類,內部維護了一個 Cleaner 物件的連結串列,通過 create(Object, Runnable) 方法建立 cleaner 物件,呼叫自身的 add 方法,將其加入到連結串列中。更重要的是提供了 clean 方法,clean 方法首先將物件自身從連結串列中刪除,保證只調用一次,然後執行 this.thunk 的 run 方法,thunk 就是由建立時傳入的 Runnable 引數,也就是說 clean 只負責觸發 Runnable 的 run 方法,至於 Runnable 做什麼任務它不關心。
那 DirectByteBuffer 傳進來的 Runnable 是什麼呢?
Deallocator 類的物件就是 DirectByteBuffer 中的 cleaner 傳進來的 Runnable 引數類,我們直接看 run 方法 unsafe.freeMemory 釋放記憶體,然後更新 Bits 裡已使用的記憶體資料。
接下來我們關注各個環節是如何串起來的?這裡主要講兩種回收方式:一種是自動回收,一種是手動回收。
如何自動回收?
Java 是不用使用者去管理記憶體的,所以 Java 對堆外記憶體 預設是自動回收的。它是 由 GC 模組負責的,在 GC 時會掃描 DirectByteBuffer 物件是否有有效引用指向該物件,如沒有,在回收 DirectByteBuffer 物件的同時且會回收其佔用的堆外記憶體。但是 JVM 如何釋放其佔用的堆外記憶體呢?如何跟 Cleaner 關聯起來呢?
這得從 Cleaner 繼承了 PhantomReference(虛引用) 說起。說到 Reference,還有 SoftReference、WeakReference、FinalReference 他們作用各不相同,這裡就不展開說了。
簡單介紹 PhantomReference,首先虛引用是不會影響 JVM 去回收其指向的物件,當 GC 某個物件時,如果有此物件上還有虛引用對其引用,會將 PhantomReference 物件插入 ReferenceQueue 佇列。
PhantomReference插入到哪個佇列呢?看 PhantomReference 類程式碼,其繼承自 Reference,Reference 物件有個 ReferenceQueue 成員,這個也就是 PhantomReference 物件插入的 ReferenceQueue 佇列,此成員如果不由外部傳入就是 ReferenceQueue.NULL。如果需要通過 queue 拿到 PhantomReference 物件,這個 ReferenceQueue 物件還是必須由外部傳入。
Reference 類內部 static 靜態塊會啟動 ReferenceHandler 執行緒,執行緒優先順序很高,這個執行緒是用來處理 JVM 在 GC 過程中交接過來的 reference。想必經常用 jstack 命令,看執行緒堆疊的同學應該見到過這個執行緒。
我們來看看 ReferenceHandler 是如何處理的?直接看 run 方法,首先是個死迴圈,一直在那不停的幹活,synchronized 塊內的這段主要是交接 JVM 扔過來的 reference(就是 pending),再往下看,很明顯,呼叫了 cleaner 的 clean 方法。調完之後直接 continue 結束此次迴圈,這個 reference 並沒有進入 queue,也就是說 Cleaner 虛引用是不放入 ReferenceQueue。
這塊有點想不通,既然不放入 ReferenceQueue,為什麼 Cleaner 類還是初始化了這個 ReferenceQueue。
如何手動回收?
手動回收,就是由開發手動呼叫 DirectByteBuffer 的 cleaner 的 clean 方法來釋放空間。由於 cleaner 是 private 反問許可權,所以自然想到使用反射來實現。
還有另一種方法,DirectByteBuffer 實現了 DirectBuffer 介面,這個介面有 cleaner 方法可以獲取 cleaner 物件。
Netty 中的堆外記憶體就是使用反射來實現手動回收方式進行回收的。
喜歡本文的朋友們,歡迎長按下圖關注訂閱號 滌生的部落格 ,收看更多精彩內容
往
再 次 剖 析 “ 一 個 J V M 參 數 引 發 的 頻 繁 C M S G C ”
一 個 J V M 參 數 引 發 的 頻 繁 C M S G C
一 次 Y o u n g G C 的 優 化 實 踐 ( F i n a l R e f e r e n c e 相 關 )
依 賴 包 濫 用 S y s t e m . g c ( ) 導 致 的 頻 繁 F u l l G C
服 務 框 架 之 注 冊 中 心 , 你 不 知 道 的 內 幕
P h a n t o m R e f e r e n c e 導 致 C M S G C 耗 時 嚴 重
T h r e a d L o c a l 實 現 原 理 詳 解