Netty之Java堆外內存掃盲貼


Java的堆外內存本來是高貴而神秘的東西,只在一些緩存方案的收費企業版里出現。但自從用了Netty,就變成了天天打交道的事情,畢竟堆外內存能減少IO時的內存復制,不需要堆內存Buffer拷貝一份到直接內存中,然后才寫入Socket中;而且也沒了煩人的GC。

好在,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(),其他類型就放入應用構造Reference時傳入的ReferenceQueue中,這樣應用的代碼可以從Queue里拖出這些理論上已死的對象,做愛做的事情——這是一種比finalizer更輕量更好的機制。

 

5. 其實

專家們說,OpenJDK沒有接受jemalloc(redis們在用)的補丁,直接用malloc在OS里申請一段內存,比在已申請好的JVM堆內內存里划一塊出來要慢,所以我們在Netty一般用池化的 PooledDirectByteBuf 對DirectByteBuffer進行重用 ,《Netty權威指南》說性能提升了23倍,所以基本不需要頭痛堆外內存的釋放,順便還告別了大數據流量下的頻繁GC。

 
文章持續修訂,轉載請保留原鏈接: http://calvin1978.blogcn.com/articles/directbytebuffer.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM