Netty之內存泄露


直接內存是IO框架的絕配,但直接內存的分配銷毀不易,所以使用內存池能大幅提高性能。

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(ByteOrder)所創建的ByteBuf,與原對象共享底下的buffer,也共享引用計數器,所以它們經常需要調用retain()來顯示自己的存在。

當引用計數器為0,底下的buffer已被回收,即使ByteBuf對象還在,對它的各種訪問操作都會拋出異常。

3.誰來負責Release

在C時代,我們喜歡讓malloc和free成對出現,而在Netty里,因為Handler鏈的存在,ByteBuf經常要傳遞到下一個Hanlder去而不復還,所以規則變成了誰是最后使用者,誰負責釋放。

另外,更要注意的是各種異常情況,ByteBuf沒有成功傳遞到下一個Hanlder,還在自己地界里的話,一定要進行釋放。

多層的異常處理機制,有些異常處理的地方不一定准確知道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這個素數來取模,命中了就創建一個Java堆外內存掃盲貼里說的PhantomReference。然后創建一個Wrapper,包住ByteBuf和Reference。

Simple級別下,wrapper只在執行release()時調用Reference.clear()把Reference清理掉,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,平時也不會真用了你的。然后監控其使用量,及時報警。


免責聲明!

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



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