Java堆外記憶體的回收機制
1 堆外記憶體
JVM啟動時分配的記憶體,稱為堆記憶體,與之相對的,在程式碼中還可以使用堆外記憶體,不如Netty,廣泛使用了堆外記憶體,但是這部分記憶體不歸JVM管理,GC演算法並不會對它們進行回收,所以使用堆外記憶體是需要格外小心,以防出現記憶體洩露。
2 堆外記憶體的申請和釋放
JDK中使用DirectByteBuffer物件來表示堆外記憶體,可以通過-XX:MaxDirectMemorySize來指定最大的堆外記憶體,每個DirectByteBuffer物件在初始化時,都會建立一個對應的Cleaner物件,在Cleaner物件回收的時候回收這部分堆外記憶體。初始化時引用關係如下:

image
其中first是Cleaner類的靜態變數,Cleaner物件在初始化時會被新增到Clener連結串列中,和first形成引用關係,ReferenceQueue是用來儲存需要回收的Cleaner物件。
3 Cleaner如何與GC相關聯
JDK除了StrongReference、SoftReference和WeakReference之外,還有一種PhantomReference是虛引用,Cleaner就是PhantomReference的子類。(針對這幾種引用,後續專題講解)
當GC時發現它除了PhantomReference外已不可達(持有它的DirectByteBuffer失效了),就會把它放進 Reference類pending list靜態變數裡。然後另有一條ReferenceHandler執行緒,名字叫 "Reference Handler"的,關注著這個pending list,如果看到有物件型別是Cleaner,就會執行它的clean(),在最終的處理裡會通過Unsafe的free介面來釋放DirectByteBuffer對應的堆外記憶體塊。
4 堆外記憶體基於GC的回收
快速回顧一下堆內的GC機制,當新生代滿了,就會發生young gc;如果此時物件還沒失效,就不會被回收;撐過幾次young gc後,物件被遷移到老生代;當老生代也滿了,就會發生full gc。
這裡可以看到一種尷尬的情況,因為DirectByteBuffer本身的個頭很小,只要熬過了young gc,即使已經失效了也能在老生代裡舒服的呆著,不容易把老生代撐爆觸發full gc,如果沒有別的大塊頭進入老生代觸發full gc,就一直在那耗著,佔著一大片堆外記憶體不釋放。
其實在初始化DirectByteBuffer物件時,如果當前堆外記憶體的條件很苛刻時,會主動呼叫System.gc()強制執行FGC。
這時,就只能靠觸發system.gc()來救場了。如果還是無法釋放,就可能會出現OOM。
不過很多線上環境的JVM引數有-XX:+DisableExplicitGC,導致了System.gc()等於一個空函式,根本不會觸發FGC,這點需要特別關注。