溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java 堆外內(nèi)存回收原理

發(fā)布時間:2020-07-31 00:46:47 來源:網(wǎng)絡 閱讀:3400 作者:Java_老男孩 欄目:編程語言

堆外內(nèi)存簡介

DirectByteBuffer 這個類是 JDK 提供使用堆外內(nèi)存的一種途徑,當然常見的業(yè)務開發(fā)一般不會接觸到,即使涉及到也可能是框架(如 Netty、RPC 等)使用的,對框架使用者來說也是透明的。

堆外內(nèi)存優(yōu)勢

堆外內(nèi)存優(yōu)勢在 IO 操作上,對于網(wǎng)絡 IO,使用 Socket 發(fā)送數(shù)據(jù)時,能夠節(jié)省堆內(nèi)存到堆外內(nèi)存的數(shù)據(jù)拷貝,所以性能更高。看過 Netty 源碼的同學應該了解,Netty 使用堆外內(nèi)存池來實現(xiàn)零拷貝技術。對于磁盤 IO 時,也可以使用內(nèi)存映射,來提升性能。另外,更重要的幾乎不用考慮堆內(nèi)存煩人的 GC 問題。

堆外內(nèi)存創(chuàng)建

我們直接來看代碼,首先向 Bits 類申請額度,Bits 類內(nèi)部維護著當前已經(jīng)使用的堆外內(nèi)存值,會 check 當前申請的大小與已經(jīng)使用的內(nèi)存大小是否超過總的堆外內(nèi)存大小(默認大小與堆內(nèi)存差不多,其實是有細微區(qū)別的,拿 CMS GC 來舉例,它的大小是新生代的最大值 - 一個 survivor 的大小 + 老生代的最大值),可以使用 -XX:MaxDirectMemorySize 參數(shù)指定堆外內(nèi)存最大大小。

Java 堆外內(nèi)存回收原理

如果 check 不通過,會主動執(zhí)行 System.gc(),然后 sleep 100 毫秒,再進行 check,如果內(nèi)存還是不足,就拋出 OOM Error。

如果 check 通過,就會調(diào)用 unsafe.allocateMemory 真正分配內(nèi)存,返回內(nèi)存地址,然后再將內(nèi)存清 0。題外話,這個 unsafe 命名看著是不是很嚇人,這個 unsafe 不是說不安全,而是 JDK 內(nèi)部使用的類,不推薦外部使用,所以叫 unsafe,Netty 源碼內(nèi)部也有類似命名。

由于申請內(nèi)存前可能會調(diào)用 System.gc(),所以謹慎設置 -XX:+DisableExplicitGC 這個選項,這個參數(shù)作用是禁止代碼中顯示觸發(fā)的 Full GC。

堆外內(nèi)存回收

cleaner = Cleaner.create(this, new Deallocator (base, size, cap));

看到這段代碼從成員的命名上就應該知道,是用來回收堆外內(nèi)存的。確實,但是它是如何工作的呢?接下來我們看看 Cleaner 類。

Java 堆外內(nèi)存回收原理

Cleaner 類,內(nèi)部維護了一個 Cleaner 對象的鏈表,通過 create(Object, Runnable) 方法創(chuàng)建 cleaner 對象,調(diào)用自身的 add 方法,將其加入到鏈表中。更重要的是提供了 clean 方法,clean 方法首先將對象自身從鏈表中刪除,保證只調(diào)用一次,然后執(zhí)行 this.thunk 的 run 方法,thunk 就是由創(chuàng)建時傳入的 Runnable 參數(shù),也就是說 clean 只負責觸發(fā) Runnable 的 run 方法,至于 Runnable 做什么任務它不關心。

那 DirectByteBuffer 傳進來的 Runnable 是什么呢?

Java 堆外內(nèi)存回收原理

Deallocator 類的對象就是 DirectByteBuffer 中的 cleaner 傳進來的 Runnable 參數(shù)類,我們直接看 run 方法 unsafe.freeMemory 釋放內(nèi)存,然后更新 Bits 里已使用的內(nèi)存數(shù)據(jù)。

接下來我們關注各個環(huán)節(jié)是如何串起來的?這里主要講兩種回收方式:一種是自動回收,一種是手動回收。

如何自動回收?

Java 是不用用戶去管理內(nèi)存的,所以 Java 對堆外內(nèi)存 默認是自動回收的。它是 由 GC 模塊負責的,在 GC 時會掃描 DirectByteBuffer 對象是否有有效引用指向該對象,如沒有,在回收 DirectByteBuffer 對象的同時且會回收其占用的堆外內(nèi)存。但是 JVM 如何釋放其占用的堆外內(nèi)存呢?如何跟 Cleaner 關聯(lián)起來呢?

這得從 Cleaner 繼承了 PhantomReference(虛引用) 說起。說到 Reference,還有 SoftReference、WeakReference、FinalReference 他們作用各不相同,這里就不展開說了。

簡單介紹 PhantomReference,首先虛引用是不會影響 JVM 去回收其指向的對象,當 GC 某個對象時,如果有此對象上還有虛引用對其引用,會將 PhantomReference 對象插入 ReferenceQueue 隊列。

PhantomReference插入到哪個隊列呢?看 PhantomReference 類代碼,其繼承自 Reference,Reference 對象有個 ReferenceQueue 成員,這個也就是 PhantomReference 對象插入的 ReferenceQueue 隊列,此成員如果不由外部傳入就是 ReferenceQueue.NULL。如果需要通過 queue 拿到 PhantomReference 對象,這個 ReferenceQueue 對象還是必須由外部傳入。

Java 堆外內(nèi)存回收原理

Reference 類內(nèi)部 static 靜態(tài)塊會啟動 ReferenceHandler 線程,線程優(yōu)先級很高,這個線程是用來處理 JVM 在 GC 過程中交接過來的 reference。想必經(jīng)常用 jstack 命令,看線程堆棧的同學應該見到過這個線程。

我們來看看 ReferenceHandler 是如何處理的?直接看 run 方法,首先是個死循環(huán),一直在那不停的干活,synchronized 塊內(nèi)的這段主要是交接 JVM 扔過來的 reference(就是 pending),再往下看,很明顯,調(diào)用了 cleaner 的 clean 方法。調(diào)完之后直接 continue 結束此次循環(huán),這個 reference 并沒有進入 queue,也就是說 Cleaner 虛引用是不放入 ReferenceQueue。

Java 堆外內(nèi)存回收原理

這塊有點想不通,既然不放入 ReferenceQueue,為什么 Cleaner 類還是初始化了這個 ReferenceQueue。

如何手動回收?

手動回收,就是由開發(fā)手動調(diào)用 DirectByteBuffer 的 cleaner 的 clean 方法來釋放空間。由于 cleaner 是 private 反問權限,所以自然想到使用反射來實現(xiàn)。

Java 堆外內(nèi)存回收原理

還有另一種方法,DirectByteBuffer 實現(xiàn)了 DirectBuffer 接口,這個接口有 cleaner 方法可以獲取 cleaner 對象。

Java 堆外內(nèi)存回收原理

Netty 中的堆外內(nèi)存池就是使用反射來實現(xiàn)手動回收方式進行回收的。


很多時候,我們在學習一門技術時候往往限于眼前而很難突破,也就是通俗意義上的思維固化,這個時候你就需要用另一種思維來打破現(xiàn)狀。學習的目的在于擴寬自己的視野、發(fā)散自己的思維,以更完備的視角去迎接人生的抉擇。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI