有過痛苦的經(jīng)歷,特別能寫出深刻的文章——凱爾文.肖
直接內(nèi)存是IO框架的絕配,但直接內(nèi)存的分配銷毀不易,所以使用內(nèi)存池能大幅提高性能。但,要重新培養(yǎng)被Java的自動(dòng)垃圾回收慣壞了的惰性。
Netty有一篇必讀的文檔官方文檔翻譯:引用計(jì)數(shù)對象,在此基礎(chǔ)上補(bǔ)充一些自己的理解和細(xì)節(jié)。
1.為什么要有引用計(jì)數(shù)器
Netty里四種主力的ByteBuf,
其中UnpooledHeapByteBuf底下的byte[]能夠依賴JVM GC自然回收;而UnpooledDirectByteBuf底下是DirectByteBuffer,如Java堆外內(nèi)存掃盲貼所述,除了等JVM GC,最好也能主動(dòng)進(jìn)行回收;而PooledHeapByteBuf和PooledDirectByteBuf,則必須要主動(dòng)將用完的byte[]/ByteBuffer放回池里,否則內(nèi)存就要爆掉。所以,Netty ByteBuf需要在JVM的GC機(jī)制之外,有自己的引用計(jì)數(shù)器和回收過程。
一下又回到了C的冰冷時(shí)代,自己malloc對象要自己free。但和C時(shí)代又不完全一樣,內(nèi)有引用計(jì)數(shù)器,外有JVM的GC,情況更為復(fù)雜。
2.引用計(jì)數(shù)器常識
計(jì)數(shù)器基于AtomicIntegerFieldUpdater,為什么不直接用AtomicInteger?因?yàn)锽yteBuf對象很多,如果都把int包一層AtomicInteger花銷較大,而AtomicIntegerFieldUpdater只需要一個(gè)全局的靜態(tài)變量。
所有ByteBuf的引用計(jì)數(shù)器初始值為1。
調(diào)用release(),將計(jì)數(shù)器減1,等于零時(shí),deallocate()被調(diào)用,各種回收。
調(diào)用retain(),將計(jì)數(shù)器加1,即使ByteBuf在別的地方被人release()了,在本Class沒喊cut之前,不要把它釋放掉。
由duplicate(), slice()和order(ByteOrder)所創(chuàng)建的ByteBuf,與原對象共享底下的buffer,也共享引用計(jì)數(shù)器,所以它們經(jīng)常需要調(diào)用retain()來顯示自己的存在。
當(dāng)引用計(jì)數(shù)器為0,底下的buffer已被回收,即使ByteBuf對象還在,對它的各種訪問操作都會(huì)拋出異常。
3.誰來負(fù)責(zé)Release
在C時(shí)代,我們喜歡讓malloc和free成對出現(xiàn),而在Netty里,因?yàn)镠andler鏈的存在,ByteBuf經(jīng)常要傳遞到下一個(gè)Hanlder去而不復(fù)還,所以規(guī)則變成了誰是最后使用者,誰負(fù)責(zé)釋放。
另外,更要注意的是各種異常情況,ByteBuf沒有成功傳遞到下一個(gè)Hanlder,還在自己地界里的話,一定要進(jìn)行釋放。
3.1
InBound Message
在AbstractNioByteChannel.NioByteUnsafe.read()處,配置好的ByteBufAllocator創(chuàng)建相應(yīng)ByteBuf并調(diào)用pipeline.fireChannelRead(byteBuf)送入Handler鏈。
根據(jù)上面的誰最后誰負(fù)責(zé)原則,每一個(gè)Handler對消息可能有三種處理方式
對原消息不做處理,調(diào)用ctx.fireChannelRead(msg)把原消息往下傳,那不用做什么釋放。
將原消息轉(zhuǎn)化為新的消息并調(diào)用ctx.fireChannelRead(newMsg)往下傳,那必須把原消息release掉。
如果已經(jīng)不再調(diào)用ctx.fireChannelRead(msg)傳遞任何消息,那更要把原消息release掉。
假設(shè)每一個(gè)Handler都把消息往下傳,Handler并也不知道誰是啟動(dòng)Netty時(shí)所設(shè)定的Handler鏈的最后一員,所以Netty會(huì)在Handler鏈的最末補(bǔ)一個(gè)TailHandler,如果此時(shí)消息仍然是ReferenceCounted類型就會(huì)被release掉。
不過如果我們的業(yè)務(wù)Hanlder不再把消息往下傳了,這個(gè)TailHandler就派不上用場。
3.2
OutBound Message
要發(fā)送的消息通常由應(yīng)用所創(chuàng)建,并調(diào)用ctx.writeAndFlush(msg)進(jìn)入Handler鏈。在每一個(gè)Handler中的處理類似InBound Message,最后消息會(huì)來到HeadHandler,再經(jīng)過一輪復(fù)雜的調(diào)用,在flush完成后終將被release掉。
3.3異常發(fā)生時(shí)的釋放
多層的異常處理機(jī)制,有些異常處理的地方不一定準(zhǔn)確知道ByteBuf之前釋放了沒有,可以在釋放前加上引用計(jì)數(shù)大于0的判斷避免異常;
有時(shí)候不清楚ByteBuf被引用了多少次,但又必須在此進(jìn)行徹底的釋放,可以循環(huán)調(diào)用reelase()直到返回true。
4.內(nèi)存泄漏檢測
所謂內(nèi)存泄漏,主要是針對池化的ByteBuf。ByteBuf對象被JVM GC掉之前,沒有調(diào)用release()去把底下的DirectByteBuffer或byte[]歸還到池里,會(huì)導(dǎo)致池越來越大。而非池化的ByteBuf,即使像DirectByteBuf那樣可能會(huì)用到System.gc(),但終歸會(huì)被release掉的,不會(huì)出大事。
Netty擔(dān)心大家一定會(huì)不小心就搞出個(gè)大新聞來,因此提供了內(nèi)存泄漏的監(jiān)測機(jī)制。
Netty默認(rèn)就會(huì)從分配的ByteBuf里抽樣出大約1%的來進(jìn)行跟蹤。如果泄漏,會(huì)有如下語句打?。?/p>
引用
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()
這句話報(bào)告有泄漏的發(fā)生,提示你用-D參數(shù),把防漏等級從默認(rèn)的simple升到advanced,具體看到被泄漏的ByteBuf創(chuàng)建的地方和被訪問的地方。
禁用(DISABLED)-完全禁止泄露檢測,省點(diǎn)消耗。
簡單(SIMPLE)-默認(rèn)等級,告訴我們?nèi)拥?%的ByteBuf是否發(fā)生了泄露,但總共一次只打印一次,看不到就沒有了。
高級(ADVANCED)-告訴我們?nèi)拥?%的ByteBuf發(fā)生泄露的地方。每種類型的泄漏(創(chuàng)建的地方與訪問路徑一致)只打印一次。
偏執(zhí)(PARANOID)-跟高級選項(xiàng)類似,但此選項(xiàng)檢測所有ByteBuf,而不僅僅是取樣的那1%。在高壓力測試時(shí),對性能有明顯影響。
實(shí)現(xiàn)細(xì)節(jié)
每當(dāng)各種ByteBufAllocator創(chuàng)建ByteBuf時(shí),都會(huì)問問是否需要采樣,Simple和Advanced級別下,就是以113這個(gè)素?cái)?shù)來取模(害我看文檔的時(shí)候還在瞎擔(dān)心,1%,萬一泄漏的地方有所規(guī)律,剛好躲過了100這個(gè)數(shù)字呢,比如都是3倍數(shù)的),命中了就創(chuàng)建一個(gè)Java堆外內(nèi)存掃盲貼里說的PhantomReference。然后創(chuàng)建一個(gè)Wrapper,包住ByteBuf和Reference。
Simple級別下,wrapper只在執(zhí)行release()時(shí)調(diào)用Reference.clear()把Reference清理掉,Advanced級別下則會(huì)記錄每一個(gè)創(chuàng)建和訪問的動(dòng)作。
當(dāng)GC發(fā)生,還沒有被clear()的Reference就會(huì)被JVM放入到之前設(shè)定的ReferenceQueue里。
在每次創(chuàng)建PhantomReference時(shí),都會(huì)順便看看有沒有因?yàn)橥泩?zhí)行release()把Reference給clear掉,在GC時(shí)被放進(jìn)了ReferenceQueue的對象,有則以"io.netty.util.ResourceLeakDetector”為logger name,寫出前面例子里的Error級別的日日志。順便說一句,Netty能自動(dòng)匹配日志框架,先找Slf4j,再找Log4j,最后找JDK logger。
重要的事說三遍
一定要盯緊log里有沒有出現(xiàn)"LEAK: "字樣,因?yàn)镾imple級別下它只會(huì)出現(xiàn)一次,所以不要依賴自己的眼睛,要依賴grep。如果出現(xiàn)了,而且你用的是PooledBuf,那一定是問題,不要有任何的僥幸,立刻用"-Dio.netty.leakDetectionLevel=advanced"再跑一次,看清楚它創(chuàng)建和最后訪問的地方。
功能測試時(shí),最好開著"-Dio.netty.leakDetectionLevel=paranoid"
但是,怎么測試都可能有沒覆蓋到的分支,如果內(nèi)存尚夠,可以適當(dāng)把-XX:MaxDirectMemorySize調(diào)大,反正只是max,平時(shí)也不會(huì)真用了你的。然后監(jiān)控其使用量,及時(shí)報(bào)警。