堆外記憶體的回收機制分析
1.堆外記憶體
JVM啟動時分配的記憶體,稱為堆記憶體,與之相對的,在程式碼中還可以使用堆外記憶體,比如Netty,廣泛使用了堆外記憶體,但是這部分的記憶體並不歸JVM管理,GC演算法並不會對它們進行回收,所以在使用堆外記憶體時,要格外小心,防止記憶體一直得不到釋放,造成線上故障。
2.堆外記憶體的申請和釋放
JDK的ByteBuffer類提供了一個介面allocateDirect(int capacity)進行堆外記憶體的申請,底層通過unsafe.allocateMemory(size)實現,接下去看看在JVM層面是如何實現的。
可以發現,最底層是通過malloc方法申請的,但是這塊記憶體需要進行手動釋放,JVM並不會進行回收,幸好Unsafe提供了另一個介面freeMemory可以對申請的堆外記憶體進行釋放。
堆外記憶體的回收機制
如果每次申請堆外記憶體,都需要在程式碼中顯示的釋放,對於Java這門語言的設計來說,顯然不夠合理,既然JVM不會管理這些堆外記憶體,它們是如何回收的呢?
DirectByteBuffer
JDK中使用DirectByteBuffer物件來表示堆外記憶體,每個DirectByteBuffer物件在初始化時,都會建立一個對用的Cleaner物件,這個Cleaner物件會在合適的時候執行unsafe.freeMemory(address),從而回收這塊堆外記憶體。
當初始化一塊堆外記憶體時,物件的引用關係如下:
其中first是Cleaner類的靜態變數,Cleaner物件在初始化時會被新增到Clener連結串列中,和first形成引用關係,ReferenceQueue是用來儲存需要回收的Cleaner物件。
如果該DirectByteBuffer物件在一次GC中被回收了。
此時,只有Cleaner物件唯一儲存了堆外記憶體的資料(開始地址、大小和容量),在下一次FGC時,把該Cleaner物件放入到ReferenceQueue中,並觸發clean方法。 Cleaner物件的clean方法主要有兩個作用:
- 把自身從Clener連結串列刪除,從而在下次GC時能夠被回收
- 釋放堆外記憶體
public void run() {
if (address == 0) {
// Paranoia return;
}
unsafe.freeMemory(address); address = 0 ; Bits.unreserveMemory(size, capacity);
}
如果JVM一直沒有執行FGC的話,無效的Cleaner物件就無法放入到ReferenceQueue中,從而堆外記憶體也一直得不到釋放,記憶體豈不是會爆? 其實在初始化DirectByteBuffer物件時,如果當前堆外記憶體的條件很苛刻時,會主動呼叫System.gc()強制執行FGC。
過很多線上環境的JVM引數有-XX:+DisableExplicitGC,導致了System.gc()等於一個空函式,根本不會觸發FGC,這一點在使用Netty框架時需要注意是否會出問題。