Netty之有效規避內存泄漏

有過痛苦的經歷,特別能寫出深入的文章 —— 凱爾文. 肖html

直接內存是IO框架的絕配,但直接內存的分配銷燬不易,因此使用內存池能大幅提升性能,也告別了頻繁的GC。但,要從新培養被Java的自動垃圾回收慣壞了的惰性。app

Netty有一篇必讀的文檔 官方文檔翻譯:引用計數對象 ,在此基礎上補充一些本身的理解和細節。框架

 

1.爲何要有引用計數器

Netty裏四種主力的ByteBuf,
其中UnpooledHeapByteBuf 底下的byte[]可以依賴JVM GC天然回收;而UnpooledDirectByteBuf底下是DirectByteBuffer,如Java堆外內存掃盲貼所述,除了等JVM GC,最好也能主動進行回收;而PooledHeapByteBuf 和 PooledDirectByteBuf,則必需要主動將用完的byte[]/ByteBuffer放回池裏,不然內存就要爆掉。因此,Netty ByteBuf須要在JVM的GC機制以外,有本身的引用計數器和回收過程。性能

一下又回到了C的冰冷時代,本身malloc對象要本身free。 但和C時代又不徹底同樣,內有引用計數器,外有JVM的GC,狀況更爲複雜。測試

 

2. 引用計數器常識

  • 計數器基於 AtomicIntegerFieldUpdater,爲何不直接用AtomicInteger?由於ByteBuf對象不少,若是都把int包一層AtomicInteger花銷較大,而AtomicIntegerFieldUpdater只須要一個全局的靜態變量。
  • 全部ByteBuf的引用計數器初始值爲1。
  • 調用release(),將計數器減1,等於零時, deallocate()被調用,各類回收。
  • 調用retain(),將計數器加1,即便ByteBuf在別的地方被人release()了,在本Class沒喊cut以前,不要把它釋放掉。
  • 由duplicate(), slice()和order()所衍生的ByteBuf,與原對象共享底下的buffer,也共享引用計數器,因此它們常常須要調用retain()來顯示本身的存在。
  • 當引用計數器爲0,底下的buffer已被回收,即便ByteBuf對象還在,對它的各類訪問操做都會拋出異常

 

3.誰來負責Release

在C時代,咱們喜歡讓malloc和free成對出現,而在Netty裏,由於Handler鏈的存在,ByteBuf常常要傳遞到下一個Hanlder去而不復還,因此規則變成了誰是最後使用者,誰負責釋放spa

另外,更要注意的是各類異常狀況,ByteBuf沒有成功傳遞到下一個Hanlder,還在本身地界裏的話,必定要進行釋放.net

3.1 InBound Message

在AbstractNioByteChannel.NioByteUnsafe.read() 處建立了ByteBuf並調用 pipeline.fireChannelRead(byteBuf) 送入Handler鏈。翻譯

根據上面的誰最後誰負責原則,每一個Handler對消息可能有三種處理方式日誌

  • 對原消息不作處理,調用 ctx.fireChannelRead(msg)把原消息往下傳,那不用作什麼釋放。
  • 將原消息轉化爲新的消息並調用 ctx.fireChannelRead(newMsg)往下傳,那必須把原消息release掉。
  • 若是已經再也不調用ctx.fireChannelRead(msg)傳遞任何消息,那更要把原消息release掉

假設每個Handler都把消息往下傳,Handler並也不知道誰是啓動Netty時所設定的Handler鏈的最後一員,因此Netty在Handler鏈的最末補了一個TailHandler,若是此時消息仍然是ReferenceCounted類型就會被release掉。
 netty

3.2 OutBound Message

要發送的消息由應用所建立,並調用 ctx.writeAndFlush(msg) 進入Handler鏈。在每一個Handler中的處理相似InBound Message,最後消息會來到HeadHandler,再通過一輪複雜的調用,在flush完成後終將被release掉

 

3.3 異常發生時的釋放

多層的異常處理機制,有些異常處理的地方不必定準確知道ByteBuf以前釋放了沒有,能夠在釋放前加上引用計數大於0的判斷避免釋放失敗;

有時候不清楚ByteBuf被引用了多少次,但又必須在此進行完全的釋放,能夠循環調用reelase()直到返回true

 

4. 內存泄漏檢測

所謂內存泄漏,主要是針對池化的ByteBuf。ByteBuf對象被JVM GC掉以前,沒有調用release()把底下的DirectByteBuffer或byte[]歸還到池裏,會致使池愈來愈大。而非池化的ByteBuf,即便像DirectByteBuf那樣可能會用到System.gc(),但終歸會被release掉的,不會出大事。

Netty擔憂你們不當心就搞出個大新聞來,所以提供了內存泄漏的監測機制。

Netty默認會從分配的ByteBuf裏抽樣出大約1%的來進行跟蹤。若是泄漏,會有以下語句打印:

 

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

這句話報告有泄漏的發生,提示你用-D參數,把防漏等級從默認的simple升到advanced,就能具體看到被泄漏的ByteBuf被建立和訪問的地方。

  • 禁用(DISABLED) - 徹底禁止泄露檢測,省點消耗。
  • 簡單(SIMPLE) - 默認等級,告訴咱們取樣的1%的ByteBuf是否發生了泄露,但總共一次只打印一次,看不到就沒有了。
  • 高級(ADVANCED) - 告訴咱們取樣的1%的ByteBuf發生泄露的地方。每種類型的泄漏(建立的地方與訪問路徑一致)只打印一次。對性能有影響。
  • 偏執(PARANOID) - 跟高級選項相似,但此選項檢測全部ByteBuf,而不只僅是取樣的那1%。對性能有絕大的影響。

實現細節

每當各類ByteBufAllocator 建立ByteBuf時,都會問問是否須要採樣,Simple和Advanced級別下,就是以113這個素數來取模(害我看文檔的時候還在瞎擔憂,1%,萬一泄漏的地方有所規律,恰好躲過了100這個數字呢,好比都是3倍數的),命中了就建立一個Java堆外內存掃盲貼裏說的PhantomReference。而後建立一個Wrapper,包住ByteBuf和Reference。

simple級別下,wrapper只在執行release()時調用Reference.clear(),Advanced級別下則會記錄每個建立和訪問的動做。

當GC發生,尚未被clear()的Reference就會被JVM放入到以前設定的ReferenceQueue裏。

在每次建立PhantomReference時,都會順便看看有沒有由於忘記執行release()把Reference給clear掉,在GC時被放進了ReferenceQueue的對象,有則以 "io.netty.util.ResourceLeakDetector」爲logger name,寫出前面例子裏的Error級別的日日誌。順便說一句,Netty能自動匹配日誌框架,先找Slf4j,再找Log4j,最後找JDK logger。

值得說三遍的事

必定要盯緊log裏有沒有出現 "LEAK: "字樣,由於simple級別下它只會出現一次,因此不要依賴本身的眼睛,要依賴grep。若是出現了,並且你用的是PooledBuf,那必定是問題,不要有任何的僥倖,馬上用"-Dio.netty.leakDetectionLevel=advanced" 再跑一次,看清楚它建立和訪問的地方。

功能測試時,最好開着"-Dio.netty.leakDetectionLevel=paranoid"。

可是,怎麼測試均可能存在沒有覆蓋到的分支。若是內存尚夠,能夠適當把-XX:MaxDirectMemorySize 調大,反正只是max,平時也不會真用了你的。而後監控其使用量,及時報警。

 
文章持續修訂,轉載請保留原連接: http://calvin1978.blogcn.com/articles/netty-leak.html

相關文章
相關標籤/搜索