JVM可以使用的內存分外2種:堆內存和堆外內存.
參考:http://www.reibang.com/p/84b175a14323(你假笨)
http://calvin1978.blogcn.com/articles/directbytebuffer.html(江南白衣)
堆外內存的創(chuàng)建
可以通過jdk nio中的ByteBuffer創(chuàng)建再愈。如下:
而真正的內存分配是使用的Bits.reserveMemory方法,如下:
在DirectByteBuffer中抗悍,首先向Bits類申請額度钳枕,Bits類有一個全局的 totalCapacity變量,記錄著全部DirectByteBuffer的總大小衔沼,每次申請昔瞧,都先看看是否超限 -- 堆外內存的限額默認與堆內內存(由-Xmx 設定)相仿,可用 -XX:MaxDirectMemorySize 重新設定自晰。
如果已經(jīng)超限,會主動執(zhí)行Sytem.gc()缘圈,期待能主動回收一點堆外內存袜蚕。然后休眠一百毫秒,看看totalCapacity降下來沒有牲剃,如果內存還是不足,就拋出大家最頭痛的OOM異常缠犀。
最后,創(chuàng)建一個Cleaner辨液,并把代表清理動作的Deallocator類綁定 -- 降低Bits里的totalCapacity,并調用Unsafe調free去釋放內存止吁。
堆外內存的回收
存在于堆內的DirectByteBuffer對象很小燎悍,只存著基地址和大小等幾個屬性,和一個Cleaner俄删,但它代表著后面所分配的一大段內存奏路,是所謂的冰山對象。堆內的DirectByteBuffer對象被GC時鸽粉,它背后的堆外內存也會被回收。
快速回顧一下堆內的GC機制秽褒,當新生代滿了威兜,就會發(fā)生young gc;如果此時對象還沒失效椒舵,就不會被回收笔宿;撐過幾次young gc后,對象被遷移到老生代泼橘;當老生代也滿了,就會發(fā)生full gc醋粟。
這里可以看到一種尷尬的情況,因為DirectByteBuffer本身的個頭很小米愿,只要熬過了young gc,即使已經(jīng)失效了也能在老生代里舒服的呆著较鼓,不容易把老生代撐爆觸發(fā)full gc违柏,如果沒有別的大塊頭進入老生代觸發(fā)full gc,就一直在那耗著,占著一大片堆外內存不釋放士鸥。
這時,就只能靠前面提到的申請額度超限時觸發(fā)的System.gc()來救場了讼积。但這道最后的保險其實也不很好脚仔,首先它會中斷整個進程,然后它讓當前線程睡了整整一百毫秒们颜,而且如果gc沒在一百毫秒內完成猎醇,它仍然會無情的拋出OOM異常。還有硫嘶,萬一大家設置了-DisableExplicitGC禁止了system.gc(),那就無法回收了称近。
所以哮塞,堆外內存還是自己主動點回收更好,比如Netty就是這么做的坛善。
Cleaner如何與GC相關聯(lián)?
DirectByteBuffer中有個成員變量Cleaner眠屎,Cleaner是PhantomReference(虛引用)的子類,PhantomReference它其實主要是用來跟蹤對象何時被回收的岖常,它不能影響gc決策葫督,但是gc過程中如果發(fā)現(xiàn)某個對象除了只有PhantomReference引用它之外,并沒有其他的地方引用它了偎快,那將會把這個引用放到java.lang.ref.Reference.pending隊列里洽胶,在gc完畢的時候通知ReferenceHandler這個守護線程去執(zhí)行一些后置處理。如果是Cleaner類型姊氓,則執(zhí)行clean方法,釋放堆外內存读跷。
ReferenceHandler是抽象類Reference的一個內部類禾唁,代碼如下:
為什么要使用堆外內存
(1)可以擴展至更大的內存空間
(2)在進行網(wǎng)絡通信的時候荡短,堆外內存能減少IO時的內存復制,不需要堆內存Buffer拷貝一份到直接內存中肢预,然后才寫入Socket中
PS:如果我們的應用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心沼本,存在潛在的內存泄露風險锭沟。