占小狼
轉載請注明原創出處,謝謝!
堆外內存
JVM啟動時分配的內存,稱為堆內存,與之相對的,在代碼中還可以使用堆外內存,比如Netty,廣泛使用了堆外內存,但是這部分的內存并不歸JVM管理,GC算法并不會對它們進行回收,所以在使用堆外內存時,要格外小心,防止內存一直得不到釋放,造成線上故障。
堆外內存的申請和釋放
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
方法主要有兩個作用:
1、把自身從Clener
鏈表刪除,從而在下次GC時能夠被回收
2、釋放堆外內存
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框架時需要注意是否會出問題。