Java堆外內存掃盲貼
Java的堆外內存本來是高貴而神秘的東西,只在一些緩存方案的收費企業版里出現。但自從用了Netty,就變成了天天打交道的事情,畢竟堆外內存能減少IO時的內存復制,不需要堆內存Buffer拷貝一份到直接內存中,然后才寫入Socket中。
好在,Netty所用的堆外內存只是Java NIO的 DirectByteBuffer類,通讀一次很快。還有一些sun.misc.*的類木有源碼,要自己跑去OpenJdk那看個明白。
1. 堆外內存的創建
在DirectByteBuffer中,首先向Bits類申請額度,Bits類有一個全局的 totalCapacity變量,記錄著全部DirectByteBuffer的總大小,每次申請,都先看看是否超限 -- 堆外內存的限額默認與堆內內存(由-XMX 設定)相仿,可用 -XX:MaxDirectMemorySize 重新設定。
如果已經超限,會主動執行Sytem.gc(),期待能主動回收一點堆外內存。然后休眠一百毫秒,看看totalCapacity降下來沒有,如果內存還是不足,就拋出大家最頭痛的OOM異常。
如果額度被批準,就調用大名鼎鼎的sun.misc.Unsafe去分配內存,返回內存基地址,Unsafe的C++實現在此,標準的malloc。然后再調一次Unsafe把這段內存給清零。跑個題,Unsafe的名字是提醒大家這個類只給Sun自家用的,你們別用,不然哪天Sun把它藏起來了你們就哭死。果然,JDK9里就Oracle可能動手哦。
JDK7開始,DirectByteBuffer分配內存時默認已不做分頁對齊,不會再每次分配并清零 實際需要+分頁大小(4k)的內存,這對性能應有較大提升,所以Oracle專門寫在了Enhancements in Java I/O里。
最后,創建一個Cleaner,并把代表清理動作的Deallocator類綁定 -- 降低Bits里的totalCapacity,并調用Unsafe調free去釋放內存。Cleaner的觸發機制后面再說。
2. 堆外內存基于GC的回收
存在于堆內的DirectByteBuffer對象很小,只存著基地址和大小等幾個屬性,和一個Cleaner,但它代表著后面所分配的一大段內 存,是所謂的冰山對象。通過前面說的Cleaner,堆內的DirectByteBuffer對象被GC時,它背后的堆外內存也會被回收。
快速回顧一下堆內的GC機制,當新生代滿了,就會發生young gc;如果此時對象還沒失效,就不會被回收;撐過幾次young gc后,對象被遷移到老生代;當老生代也滿了,就會發生full gc。
這里可以看到一種尷尬的情況,因為DirectByteBuffer本身的個頭很小,只要熬過了young gc,即使已經失效了也能在老生代里舒服的呆著,不容易把老生代撐爆觸發full gc,如果沒有別的大塊頭進入老生代觸發full gc,就一直在那耗著,占著一大片堆外內存不釋放。
這時,就只能靠前面提到的申請額度超限時觸發的system.gc()來救場了。但這道最后的保險其實也不很好,首先它會中斷整個進程,然后它讓當 前線程睡了整整一百毫秒,而且如果gc沒在一百毫秒內完成,它仍然會無情的拋出OOM異常。還有,萬一,萬一大家迷信某個調優指南設置了 -DisableExplicitGC禁止了system.gc(),那就不好玩了。
所以,堆外內存還是自己主動點回收更好,比如Netty就是這么做的。
3. 堆外內存的主動回收
對于Sun的JDK這其實很簡單,只要從DirectByteBuffer里取出那個sun.misc.Cleaner,然后調用它的clean()就行。
前面說的,clean()執行時實際調用的是被綁定的Deallocator類,這個類可被重復執行,釋放過了就不再釋放。所以GC時再被動執行一次clean()也沒所謂。
在Netty里,因為不確定跑在Sun的JDK里(比如安卓),所以多廢了些功夫來確定Cleaner的存在。
4. Cleaner如何與GC相關聯?
漲知識的時間到了,原來JDK除了StrongReference,SoftReference 和 WeakReference之外,還有一種PhantomReference,Phantom是幻影的意思,Cleaner就是PhantomReference的子類。
當GC時發現它除了PhantomReference外已不可達(持有它的DirectByteBuffer失效了),就會把它放進 Reference類pending list靜態變量里。然后另有一條ReferenceHandler線程,名字叫 "Reference Handler"的,關注著這個pending list,如果看到有對象類型是Cleaner,就會執行它的clean()。
5. 其實
專家們說,OpenJDK沒有接受jemalloc(redis 們在用)的補丁,直接用malloc在OS里申請一段內存,比在已申請好的JVM堆內內存里劃一塊出來要慢,所以我們在Netty一般用池化的 PooledDirectByteBuf 對DirectByteBuffer進行重用 ,《Netty權威指南》說性能提升了23倍,所以基本不需要頭痛堆外內存的釋放。
文章持續修訂,轉載請保留原鏈接: http://calvin1978.blogcn.com/articles/directbytebuffer.html