作為 Java 開發(fā)者,你需要了解的堆外內(nèi)存知識
1. 引言
很久沒有遇到堆外內(nèi)存相關(guān)的問題了,五一假期剛結(jié)束,便不期而遇,以前也處理過幾次這類問題,但都沒有總結(jié),覺得是時候總結(jié)一下了。
先來看一個 Demo:在 Demo 中分配堆外內(nèi)存用的是 allocateDirect 方法,但其內(nèi)部調(diào)用的是 DirectByteBuffer,換言之,DirectByteBuffer 才是實際操作堆外內(nèi)存的類,因此,本場 Chat 將圍繞 DirectByteBuffer 展開。
- import java.nio.ByteBuffer;public class Demo { public static void main( String[] args )
- { //分配一塊1024Bytes的堆外內(nèi)存(直接內(nèi)存)
- //allocateDirect方法內(nèi)部調(diào)用的是DirectByteBuffer
- ByteBuffer buffer=ByteBuffer.allocateDirect(1024);
- System.out.println(buffer.capacity()); //向堆外內(nèi)存中讀寫數(shù)據(jù)
- buffer.putInt(0,2018);
- System.out.println(buffer.getInt(0));
- }
- }
2. 什么是堆外內(nèi)存?
Java 開發(fā)者一般都知道堆內(nèi)存,但卻未必了解堆外內(nèi)存。事實上,除了堆內(nèi)存,Java 還可以使用堆外內(nèi)存,也稱直接內(nèi)存(Direct Memory)。
顧名思義,堆外內(nèi)存是在 JVM Heap 之外分配的內(nèi)存塊,并不是 JVM 規(guī)范中定義的內(nèi)存區(qū)域,堆外內(nèi)存用得并不多,但十分重要。
讀者也許會有一個疑問:既然已經(jīng)有堆內(nèi)存,為什么還要用堆外內(nèi)存呢?這主要是因為堆外內(nèi)存在 IO 操作方面的優(yōu)勢。
舉一個例子:在通信中,將存在于堆內(nèi)存中的數(shù)據(jù) flush 到遠(yuǎn)程時,需要首先將堆內(nèi)存中的數(shù)據(jù)拷貝到堆外內(nèi)存中,然后再寫入 Socket 中;
如果直接將數(shù)據(jù)存到堆外內(nèi)存中就可以避免上述拷貝操作,提升性能。類似的例子還有讀寫文件。
目前,很多 NIO 框架 (如 netty,rpc) 會采用 Java 的 DirectByteBuffer 類來操作堆外內(nèi)存,DirectByteBuffer 類對象本身位于 Java 內(nèi)存模型的堆中,由 JVM 直接管控、操縱。
但是,DirectByteBuffer 中用于分配堆外內(nèi)存的方法 unsafe.allocateMemory(size) 是個一個 native 方法,本質(zhì)上是用 C 的 malloc 來進(jìn)行分配的。
分配的內(nèi)存是系統(tǒng)本地的內(nèi)存,并不在 Java 的內(nèi)存中,也不屬于 JVM 管控范圍,所以在 DirectByteBuffer 一定會存在某種特別的方式來操縱堆外內(nèi)存。
3. 堆外內(nèi)存創(chuàng)建過程深度解析
首先,我們來看一下 DirectByteBuffer 源代碼,從中洞悉分配堆外內(nèi)存的過程:
3.1 ***個重要方法:
- Bits.reserveMemory(size, cap);
源代碼如下:
該方法用于在系統(tǒng)中保存總分配內(nèi)存(按頁分配)的大小和實際內(nèi)存的大小,具體執(zhí)行中需要首先用 tryReserveMemory 方法來判斷系統(tǒng)內(nèi)存(堆外內(nèi)存)是否足夠,具體代碼如下:
從 Bits.reserveMemory(size, cap) 源碼可以看出,其執(zhí)行過程中,可能遇到以下三種情況:
1. 最樂觀的情況:可用堆外內(nèi)存足夠,reserveMemory 方法返回 true,該方法結(jié)束。
2. 如果不幸,堆外內(nèi)存不足,則須進(jìn)行第二步:
- jlra.tryHandlePendingReference()
會觸發(fā)一次非堵塞的
Reference#tryHandlePending(false),該方法會將已經(jīng)被 JVM 垃圾回收的 DirectBuffer 對象的堆外內(nèi)存釋放。
3. 如果在進(jìn)行一次堆外內(nèi)存資源回收后,還不夠進(jìn)行本次堆外內(nèi)存分配的話,則進(jìn)行 GC 操作:
System.gc() 會觸發(fā)一個 Full GC,當(dāng)然,前提是你沒有顯示的設(shè)置 - XX:+DisableExplicitGC 來禁用顯式 GC。同時,需要注意的是,調(diào)用 System.gc() 并不能夠保證 Full GC 馬上就能被執(zhí)行。
調(diào)用 System.gc() 后,接下來會最多進(jìn)行 9 次循環(huán)嘗試,仍然通過 tryReserveMemory 方法來判斷是否有足夠的堆外內(nèi)存可供分配操作。每次嘗試都會 sleep,以便 Full GC 能夠完成,如下代碼所示。
4. 最不幸的情況,經(jīng)過 9 次循環(huán)嘗試后,如果仍然沒有足夠的堆外內(nèi)存,將拋出 OutOfMemoryError 異常。
綜上所述,Bits.reserveMemory(size, cap) 方法將依次執(zhí)行以下操作:
1.如果可用堆外內(nèi)存足以分配給當(dāng)前要創(chuàng)建的堆外內(nèi)存大小時,直接返回 True;
2.如果堆外內(nèi)存不足,則觸發(fā)一次非堵塞的 Reference#tryHandlePending(false)。該方法會將已經(jīng)被 JVM 垃圾回收的 DirectBuffer 對象的堆外內(nèi)存釋放;
3.如果進(jìn)行一次堆外內(nèi)存資源回收后,還不夠進(jìn)行本次堆外內(nèi)存分配的話,則進(jìn)行 System.gc()。
System.gc() 會觸發(fā)一個 Full GC,需要注意的是,調(diào)用 System.gc() 并不能夠保證 Full GC 馬上就能被執(zhí)行。
所以在后面打代碼中,會進(jìn)行最多 9 次嘗試,看是否有足夠的可用堆外內(nèi)存來分配堆外內(nèi)存。
并且每次嘗試之前,都對延遲等待時間,已給 JVM 足夠的時間去完成 Full GC 操作。
4.如果 9 次嘗試后依舊沒有足夠的可用堆外內(nèi)存來分配本次堆外內(nèi)存,則拋出 OutOfMemoryError(“Direct buffer memory”) 異常。
3.2 第二個重要方法:
unsafe.allocateMemory(size)
......
3.3 第三個重要方法:
Cleaner.create(this, new Deallocator(base, size, cap))
......