原文:http://www.itdecent.cn/p/35cf0f348275
堆外內(nèi)存
JVM啟動(dòng)時(shí)分配的內(nèi)存,稱(chēng)為堆內(nèi)存,與之相對(duì)的,在代碼中還可以使用堆外內(nèi)存,比如Netty,廣泛使用了堆外內(nèi)存,但是這部分的內(nèi)存并不歸JVM管理,GC算法并不會(huì)對(duì)它們進(jìn)行回收,所以在使用堆外內(nèi)存時(shí),要格外小心,防止內(nèi)存一直得不到釋放,造成線上故障。
堆外內(nèi)存的申請(qǐng)和釋放
JDK的ByteBuffer類(lèi)提供了一個(gè)接口allocateDirect(int capacity)進(jìn)行堆外內(nèi)存的申請(qǐng),底層通過(guò)unsafe.allocateMemory(size)實(shí)現(xiàn),接下去看看在JVM層面是如何實(shí)現(xiàn)的。

可以發(fā)現(xiàn),最底層是通過(guò)malloc方法申請(qǐng)的,但是這塊內(nèi)存需要進(jìn)行手動(dòng)釋放,JVM并不會(huì)進(jìn)行回收,幸好Unsafe提供了另一個(gè)接口freeMemory可以對(duì)申請(qǐng)的堆外內(nèi)存進(jìn)行釋放。

堆外內(nèi)存的回收機(jī)制
如果每次申請(qǐng)堆外內(nèi)存,都需要在代碼中顯示的釋放,對(duì)于Java這門(mén)語(yǔ)言的設(shè)計(jì)來(lái)說(shuō),顯然不夠合理,既然JVM不會(huì)管理這些堆外內(nèi)存,它們是如何回收的呢?
DirectByteBuffer
JDK中使用DirectByteBuffer對(duì)象來(lái)表示堆外內(nèi)存,每個(gè)DirectByteBuffer對(duì)象在初始化時(shí),都會(huì)創(chuàng)建一個(gè)對(duì)用的Cleaner對(duì)象,這個(gè)Cleaner對(duì)象會(huì)在合適的時(shí)候執(zhí)行unsafe.freeMemory(address),從而回收這塊堆外內(nèi)存。
當(dāng)初始化一塊堆外內(nèi)存時(shí),對(duì)象的引用關(guān)系如下:

其中first是Cleaner類(lèi)的靜態(tài)變量,Cleaner對(duì)象在初始化時(shí)會(huì)被添加到Clener鏈表中,和first形成引用關(guān)系,ReferenceQueue是用來(lái)保存需要回收的Cleaner對(duì)象。
如果該DirectByteBuffer對(duì)象在一次GC中被回收了

此時(shí),只有Cleaner對(duì)象唯一保存了堆外內(nèi)存的數(shù)據(jù)(開(kāi)始地址、大小和容量),在下一次FGC時(shí),把該Cleaner對(duì)象放入到ReferenceQueue中,并觸發(fā)clean方法。
Cleaner對(duì)象的clean方法主要有兩個(gè)作用:
1、把自身從Clener鏈表刪除,從而在下次GC時(shí)能夠被回收
2、釋放堆外內(nèi)存
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
如果JVM一直沒(méi)有執(zhí)行FGC的話(huà),無(wú)效的Cleaner對(duì)象就無(wú)法放入到ReferenceQueue中,從而堆外內(nèi)存也一直得不到釋放,內(nèi)存豈不是會(huì)爆?
其實(shí)在初始化DirectByteBuffer對(duì)象時(shí),如果當(dāng)前堆外內(nèi)存的條件很苛刻時(shí),會(huì)主動(dòng)調(diào)用System.gc()強(qiáng)制執(zhí)行FGC。

不過(guò)很多線上環(huán)境的JVM參數(shù)有-XX:+DisableExplicitGC,導(dǎo)致了System.gc()等于一個(gè)空函數(shù),根本不會(huì)觸發(fā)FGC,這一點(diǎn)在使用Netty框架時(shí)需要注意是否會(huì)出問(wèn)題。