1. 程式人生 > >Java 堆外內存回收原理

Java 堆外內存回收原理

業務 有效 數據拷貝 任務 direct 性能 老生代 tom 同時

原文: https://mp.weixin.qq.com/s?__biz=MzUyMDE1ODQ3NQ==&mid=2247483773&idx=1&sn=24f9eb05ebb39642de4b4951c6b11eaf&chksm=f9efed19ce98640fb65a87b82a85f78fa1eed0e2b5229a4d49a7c17baac95fe5c3ed29086c96&token=1716214908&lang=zh_CN

堆外內存簡介
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 中的堆外內存就是使用反射來實現手動回收方式進行回收的。

Java 堆外內存回收原理