在JAVA NIO相關的組件中,ByteBuffer是除了Selector、Channel以外的另外一個很重要的組件,它是直接和Channel打交道的緩衝區,一般場景或是從ByteBuffer寫入Channel,或是從Channel讀入Buffer;而在Netty中,被精心設計的ByteBuf則是Netty貫穿整個開發過程當中的核心緩衝區,那麼他們倆有什麼區別呢?Netty對於緩衝區的設計對於高性能應用又帶來了哪些值得借鑑的思路呢?本文在介紹ByteBuffer和ByteBuf基本概念的基礎之上對二者進行對比,進而擴展介紹Netty中的ByteBuf你們族。java
JAVA NIO之ByteBuffer算法
JAVA NIO中,Channel做爲通往具備I/O操做屬性的實體的抽象,這裏的I/O操做一般指readding/writing,而具備I/O操做屬性的實體好比I/O設備、文件、網絡套接字等等。光有Channel可不行,咱們必須爲他增長readding/writing的特性,所以JAVA NIO基於Channel擴展WritableByteChannel和ReadableByteChannel接口。因爲本文的重點是ByteBuffer,所以咱們對於Channel的設計就看到這裏,由於有了WritableByteChannel和ReadableByteChannel以後,咱們就能夠對ByteBuffer進行操做啦,看看他們提供的兩個接口:
數組
public int read(ByteBuffer dst) throws IOException; public int write(ByteBuffer src) throws IOException;
從上面的接口咱們能夠看到Channel和ByteBuffer之間發生的兩個基本行爲,即readding/writing。不管是對文件(FileChannel)仍是對網絡(SocketChannel)的讀寫,他們都會去實現這兩個基本行爲。好了,咱們已經從整體上認識ByteBuffer在JAVA NIO所處的位置和擔當的角色了,下面咱們繼續深刻一點認識ByteBuffer。網絡
ByteBuffer有四個重要的屬性,分別爲:mark、position、limit、capacity,和兩個重要方法分別爲:flip和clear。ByteBuffer的底層存儲結構對於堆內存和直接內存分別表現爲堆上的一個byte[]對象和直接內存上分配的一塊內存區域。既然是一塊內存區域,那麼咱們就能夠對其進行基於字節的讀和寫,而ByteBuffer的四個int類型的屬性則是指向這塊區域的指針:app
position:讀寫指針,表明當前讀或寫操做的位置,這個值老是小於等於limit的。框架
mark:在使用ByteBuffer的過程當中,若是想要記住當前的position,則會將當前的position值給mark,讓須要恢復的時候,再將mark的值給position。性能
capacity:表明這塊內存區域的大小。spa
limit:初始的Buffer中,limit和capacity的值是相等的,一般在clear操做和flip操做的時候會對這個值進行操做,在clear操做的時候會將這個值和capacity的值設置爲相等,當flip的時候會將當前的position的值給limit,咱們能夠總結在寫的時候,limit的值表明最大的可寫位置,在讀的時候,limit的值表明最大的可讀位置。clear是爲了寫做準備、flip是爲了讀作準備。.net
ByteBuffer指針示意圖設計
在JAVA NIO中,原生的ByteByffer家族成員很簡單,主要是HeapByteBuffer、DirectByteBuffer和MappedByteBuffer:
HeapByteBuffer是基於堆上字節數組爲存儲結構的緩衝區。
DirectByteBuffer是基於直接內存上的內存區域爲存儲結構的緩衝區。
MappedByteBuffer主要是文件操做相關的,它提供了一種基於虛擬內存映射的機制,使得咱們能夠像操做文件同樣來操做文件,而不須要每次將內容更新到文件之中,同時讀寫效率很是高。
Netty之ByteBuf
相比於ByteBuffer的讀寫指針position,ByteBuf提供了兩個指針readerIndex和writeIndex來分別指向讀的位置和寫的位置,不須要每次爲讀寫作準備,直接設置讀寫指針進行讀寫操做便可。咱們看看處於中間狀態的狀態:
讀寫中間狀態的Buffer
從開始到readerIndex指針之間的這塊區域是能夠被丟棄的區域,後面會講到,readerIndex和writerIndex指針之間的區域是能夠被讀的,writerIndex和capacity指針之間的區域是能夠寫的區域。當writerIndex指針到達頂端以後,ByteBuf容許用戶複用以前已經被讀過的區域,調用discardReadBytes方法便可,對應於上面的狀態,調用discardReadBytes以後的狀態以下:
調用discardReadBytes以後回收可用區域
除了discardReadBytes方法以外,另一個比較重要的方法就是clear了,clear即清除緩衝區的指針狀態,回覆到初始值,對應於中間狀態的那張圖,調用clear以後的狀態以下:
調用clear以後,Buffer狀態的指針狀態獲得了初始化
Netty ByteBuf的特色
這裏想要比較兩種Buffer,對比ByteBuffer得出ByteBuf的優勢點,咱們首先要作的就是總結ByteBuf的特色以及相比ByteBuffer,這個特色如何成爲優勢:
(1)ByteBuf讀寫指針
在ByteBuffer中,讀寫指針都是position,而在ByteBuf中,讀寫指針分別爲readerIndex和writerIndex,直觀看上去ByteBuffer僅用了一個指針就實現了兩個指針的功能,節省了變量,可是當對於ByteBuffer的讀寫狀態切換的時候必需要調用flip方法,而當下一次寫以前,必需要將Buffe中的內容讀完,再調用clear方法。每次讀以前調用flip,寫以前調用clear,這樣無疑給開發帶來了繁瑣的步驟,並且內容沒有讀完是不能寫的,這樣很是不靈活。相比之下咱們看看ByteBuf,讀的時候僅僅依賴readerIndex指針,寫的時候僅僅依賴writerIndex指針,不需每次讀寫以前調用對應的方法,並且沒有必須一次讀完的限制。
(2)ByteBuf引用計數
ByteBuf擴展了ReferenceCountered接口,這個接口定義的功能主要是引用計數:
ReferenceCountered接口定義
也就是全部對ByteBuf的實現,都要實現引用計數,Netty對Buffer資源進行了顯式的管理,這部分要結合Netty的內存池技術理解,當Buffer引用+1的時候,須要調用retain來讓refCnt+1,當Buffer引用數-1的時候須要調用release來讓refCnt-1,當refCnt變爲0的時候Netty爲pooled和unpooled的不一樣buffer提供了不一樣的實現,一般對於非內存池的用法,Netty把Buffer的內存回收交給了垃圾回收器,對於內存池的用法,Netty對內存的回收其實是回收到內存池內,以提供下一次的申請所使用,關於內存池這部分能夠參考我以前的一篇文章。
(3)池化Buffer資源
因爲Netty是一個NIO網絡框架,所以對於Buffer的使用若是基於直接內存(DirectBuffer)實現的話,將會大大提升I/O操做的效率,然而DirectBuffer和HeapBuffer相比之下除了I/O操做效率高以外還有一個天生的缺點,即對於DirectBuffer的申請相比HeapBuffer效率更低,所以Netty結合引用計數實現了PolledBuffer,即池化的用法,當引用計數等於0的時候,Netty將Buffer回收致池中,在下一次申請Buffer的沒某個時刻會被複用。Netty這樣作的基本想法是咱們花了很大的力氣申請了一塊內存,不能輕易讓他被回收呀,能重複利用固然重複利用咯。
(3)ByteBuffer才能和Channel打交道
歸根結底,站在NIO的立場上全部的緩衝區要想和Channel打交道,換句話說也就是從網絡Channel讀取數據的時候,都是從Channel到ByteBuffer,從緩衝區寫的網上上的時候,都是從ByteBuffer到Channel。所以,當Netty監聽到I/O讀事件的時候,會將本身流從Channel讀到ByteBuffer而不是ByteBuf,see below:
return in.read((ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length));
上面是ByteBuf的其中一個具體的讀實現,能夠看出ByteBuf維護着一個內部的ByteBuffer,叫作internalNioBuffer。當須要將字節流寫入網絡的時候,須要將ByteBuf轉換爲ByteBuffer,see below:
ByteBuffer tmpBuf; if (internal) { tmpBuf = internalNioBuffer(); } else { tmpBuf = ByteBuffer.wrap(array); } return out.write((ByteBuffer) tmpBuf.clear().position(index).limit(index + length)); }
上面是ByteBuf的其中一個具體的寫實現,在寫以前,總會將ByteBuf變成ByteBuffer。
稍微總結下這一節,ByteBuf自己的設計,在指針方面用兩個讀寫指針分別表明讀和寫指針,這樣作減小了Buffer使用的難度和出錯率,概念上去理解也比較簡單。在Netty中,每一個被申請的Buffer對於Netty來講均可能是很寶貴的資源,所以爲了得到對於內存的申請與回收更多的控制權,Netty本身根據引用計數法去實現了內存的管理,另外配合精心設計的池化算法在更大程度上控制了內存的使用,雖然相比單純的申請-使用-釋放來講實現可被管理、可被池化的Buffer是略複雜的,可是能爲Netty卓越的性能數據作一些貢獻,這絕對是值得的。最後咱們要理清概念,JAVA NIO中和Channel打交道的只能是ByteBuffer,Netty在讀寫以前都有作轉換,所以不要搞混,ByteBuf仍是ByteBuf,它不是ByteBuffer。
Netty的Buffer你們族
這一節介紹一下Netty的Buffer你們族,ByteBuf的家族是龐大的,可是咱們能夠理清套路來將他們歸類一下,這樣看起來就不會那麼的複雜,Netty主要圍繞着2*2的維度進行對Buffer的擴展,他們分別是:
DirectBuffer
HeapBuffer
PooledBuffer
UnPooledBuffer
最高層的抽象是ByteBuf,Netty首先根據直接內存和堆內存,將Buffer按照這兩個方向去擴展,以後再分別對具體的直接內存和堆內存緩衝區按照是否池話這兩個方向再進行擴展。除了這兩個維度,Netty還擴展了基於Unsafe的Buffer,咱們分別挑出一個比較典型的實現來進行介紹:
PooledHeapByteBuf:池化的基於堆內存的緩衝區。
PooledDirectByteBuf:池化的基於直接內存的緩衝區。
PooledUnsafeDirectByteBuf:池化的基於Unsafe和直接內存實現的緩衝區。
UnPooledHeapByteBuf:非池化的基於堆內存的緩衝區。
UnPooledDirectByteBuf:非池化的基於直接內存的緩衝區。
UnPooledUnsafeDirectByteBuf:非池化的基於Unsafe和直接內存實現的緩衝區。
除了上面這些,另外Netty的Buffer家族還有CompositeByteBuf、ReadOnlyByteBufferBuf、ThreadLocalDirectByteBuf等等,這裏還要說一下UnsafeBuffer,噹噹前平臺支持Unsafe的時候,咱們就可使用UnsafeBuffer,JAVA DirectBuffer的實現也是基於unsafe來對內存進行操做的,咱們能夠看到不一樣的地方是PooledUnsafeDirectByteBuf或UnPooledUnsafeDirectByteBuf維護着一個memoryAddress變量,這個變量表明着緩衝區的內存地址,在使用的過程當中加上一個offer就能夠對內存進行靈活的操做。總的來講,Netty圍繞着ByteBuf及其父接口定義的行爲分別從是直接內存仍是使用堆內存,是池話仍是非池化,是否支持Unsafe來對ByteBuf進行不一樣的擴展實現。
參考文獻:
《Netty4.0.24.Final源碼及其註釋》
《JAVA7的NIO包源碼》