JVM可以使用的內(nèi)存分外2種:堆內(nèi)存和堆外內(nèi)存.
參考:http://www.itdecent.cn/p/84b175a14323(你假笨)
http://calvin1978.blogcn.com/articles/directbytebuffer.html(江南白衣)
堆外內(nèi)存的創(chuàng)建
可以通過jdk nio中的ByteBuffer創(chuàng)建。如下:



而真正的內(nèi)存分配是使用的Bits.reserveMemory方法,如下:

在DirectByteBuffer中,首先向Bits類申請額度,Bits類有一個全局的 totalCapacity變量,記錄著全部DirectByteBuffer的總大小,每次申請,都先看看是否超限 -- 堆外內(nèi)存的限額默認(rèn)與堆內(nèi)內(nèi)存(由-Xmx 設(shè)定)相仿,可用 -XX:MaxDirectMemorySize 重新設(shè)定。
如果已經(jīng)超限,會主動執(zhí)行Sytem.gc(),期待能主動回收一點堆外內(nèi)存。然后休眠一百毫秒,看看totalCapacity降下來沒有,如果內(nèi)存還是不足,就拋出大家最頭痛的OOM異常。
最后,創(chuàng)建一個Cleaner,并把代表清理動作的Deallocator類綁定 -- 降低Bits里的totalCapacity,并調(diào)用Unsafe調(diào)free去釋放內(nèi)存。
堆外內(nèi)存的回收
存在于堆內(nèi)的DirectByteBuffer對象很小,只存著基地址和大小等幾個屬性,和一個Cleaner,但它代表著后面所分配的一大段內(nèi)存,是所謂的冰山對象。堆內(nèi)的DirectByteBuffer對象被GC時,它背后的堆外內(nèi)存也會被回收。
快速回顧一下堆內(nèi)的GC機制,當(dāng)新生代滿了,就會發(fā)生young gc;如果此時對象還沒失效,就不會被回收;撐過幾次young gc后,對象被遷移到老生代;當(dāng)老生代也滿了,就會發(fā)生full gc。
這里可以看到一種尷尬的情況,因為DirectByteBuffer本身的個頭很小,只要熬過了young gc,即使已經(jīng)失效了也能在老生代里舒服的呆著,不容易把老生代撐爆觸發(fā)full gc,如果沒有別的大塊頭進入老生代觸發(fā)full gc,就一直在那耗著,占著一大片堆外內(nèi)存不釋放。
這時,就只能靠前面提到的申請額度超限時觸發(fā)的System.gc()來救場了。但這道最后的保險其實也不很好,首先它會中斷整個進程,然后它讓當(dāng)前線程睡了整整一百毫秒,而且如果gc沒在一百毫秒內(nèi)完成,它仍然會無情的拋出OOM異常。還有,萬一大家設(shè)置了-DisableExplicitGC禁止了system.gc(),那就無法回收了。
所以,堆外內(nèi)存還是自己主動點回收更好,比如Netty就是這么做的。
Cleaner如何與GC相關(guān)聯(lián)?
DirectByteBuffer中有個成員變量Cleaner,Cleaner是PhantomReference(虛引用)的子類,PhantomReference它其實主要是用來跟蹤對象何時被回收的,它不能影響gc決策,但是gc過程中如果發(fā)現(xiàn)某個對象除了只有PhantomReference引用它之外,并沒有其他的地方引用它了,那將會把這個引用放到j(luò)ava.lang.ref.Reference.pending隊列里,在gc完畢的時候通知ReferenceHandler這個守護線程去執(zhí)行一些后置處理。如果是Cleaner類型,則執(zhí)行clean方法,釋放堆外內(nèi)存。
ReferenceHandler是抽象類Reference的一個內(nèi)部類,代碼如下:

為什么要使用堆外內(nèi)存
(1)可以擴展至更大的內(nèi)存空間
(2)在進行網(wǎng)絡(luò)通信的時候,堆外內(nèi)存能減少IO時的內(nèi)存復(fù)制,不需要堆內(nèi)存Buffer拷貝一份到直接內(nèi)存中,然后才寫入Socket中
PS:如果我們的應(yīng)用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潛在的內(nèi)存泄露風(fēng)險。