正如之前所說,網絡傳輸的基本機關是位元組。Java NIO 提供了ByteBuffer作為它的容器,但是這個類使用起來比較複雜和麻煩。Netty提供了一個更好的實作:ByteBuf。
ByteBuf的API
Netty為資料處理提供的API通過抽象類
ByteBuf
和接口
ByteBufHolder
暴露出來。
下面列出ByteBuf API的優點:
- 可擴充到使用者定義的buffer類型中
- 通過内置的複合buffer類型實作透明的零拷貝(zero-copy)
- 容量可以根據需要擴充
- 切換讀寫模式不需要調用
方法ByteBUffer.flip()
- 讀寫采用不同的索引
- 支援方法連結調用
- 支援引用計數
- 支援池技術(比如:線程池、資料庫連接配接池)
ByteBuf類—Netty的資料容器
因為所有的網絡通信都涉及到位元組序列的移動,一個有效而易用的資料結構是非常必要的。Netty的ByteBuf實作達到并超過這些需求。下面了解一下如何通過索引來簡化對擷取它持有資料的操作。
工作原理
ByteBuf維護兩個不同的索引:讀索引和寫索引。當你從ByteBuf中讀,它的
readerIndex
增加了讀取的位元組數;同理,當你向ByteBuf中寫,
writerIndex
增加。下圖顯示了一個空的ByteBuf的布局和狀态:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yN5IDO3YjNmBjMlBDO4U2YyYzX0MzM1YTMzEzLcdDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
為了了解這些索引的關系,考慮一下如果你讀資料時
readerIndex
已經和
writerIndex
一樣會發生什麼。在這個點,你已經讀完了可讀的資料。嘗試繼續往下讀會引發一個
IndexOutOfBoundsException
,就像引發數組越界那樣。
ByteBuf中名稱以read或write開頭的方法會推進相應的索引,而以set或get開頭的不會。
可以指定ByteBuf最大的容量,預設是
Integer.MAX_VALUE
。
ByteBuf的使用模式
為了了解它的使用模式,我們得首先記住上圖所展示的内容—一個數組以及兩個索引來控制讀和寫。
堆緩沖區(HEAP BUFFER)
最常使用的ByteBuf模式将資料儲存到JVM的堆中。被稱為支援數組(backing array),這個模式提供了在沒有使用池技術的情況下快速配置設定和釋放(在堆緩沖區中)。這種方法是非常适合于來處理内置資料(legacy data,直譯遺留資料,個人覺得這裡翻譯為内置資料更好了解一些,如果不合适,以後再改)的,如下所示:
ByteBuf heapBuf = ...;
if (heapBuf.hasArray()){//檢查是否有支援數組
byte[] array = heapBuf.array(); //得到支援數組
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();//計算第一個位元組的偏移量
int length = heapBuf.readableBytes();//計算可讀位元組數
handleArray(array, offset, length); //調用你的方法來處理這個array
}
當hasArray()傳回false時嘗試通路支援數組會抛出UnsupportedOperationException。這個用法與JDK的ByteBuffer類似
直接緩沖區(DIRECT BUFFER)
我們認為建立對象時記憶體總是從堆中配置設定?但并非總是如此。在JDK1.4中引入的NIO的ByteBuffer類允許JVM 通過本地(native)方法調用配置設定記憶體,其目的是通過免去中間交換的記憶體拷貝, 提升IO處理速度。
直接緩沖區的内容可以駐留在垃圾回收掃描的堆區以外。這就解釋了為什麼直接緩沖區資料對網絡資料傳輸來說是一種非常理想的方式。如果你的資料是存放在堆中配置設定的緩沖區,那麼實際上,在通過 socket 發送資料之前,JVM需要将先資料複制到直接緩沖區。
這種方式的主要缺點是對于配置設定和釋放記憶體空間來說比堆緩沖區消耗更大。如果你要處理内置資料的代碼時可能會遇到另一個缺點:因為資料沒有被配置設定到堆上,你可能需要做一個拷貝,如下所示:
ByteBuf directBuf = ...
if (!directBuf.hasArray()) {//false表示為這是直接緩沖
int length = directBuf.readableBytes();//得到可讀位元組數
byte[] array = new byte[length]; //配置設定一個具有length大小的數組
directBuf.getBytes(directBuf.readerIndex(), array); //将緩沖區中的資料拷貝到這個數組中
handleArray(array, 0, length); //下一步處理
}
明顯這要比使用支援數組的方式需要更多的工作,是以你如果提前知道資料會以一個數組的方式存取,推薦你使用堆記憶體。
複合緩沖區(COMPOSITE BUFFER)
第三種也是最後一種模式使用一個複合緩沖區,為多個ByteBuf提供一個組合的視圖。你可以添加或者删除ByteBuf執行個體,一種JDK ByteBuffer中完全沒有的實作。
Netty通過ByteBuf的子類-
CompositeByteBuf
來實作這種模式,提供了将多個buffer虛拟成一個合并的Buffer的技術。
注意:CompositeByteBuf中的ByteBuf執行個體可能同時包含堆緩沖區的和直接緩沖區的。如果CompositeByteBuf隻含有一個執行個體,調用hasArray()方法會傳回這個執行個體的hasArray()方法的值;否則總是傳回false。
讓我們考慮一條由兩部分組成的消息,header和body,通過HTTP傳輸。這兩部分由不同的應用程式子產品産生,這個應用有為多條消息重用同一個body的選項。在這種情況下,會為每條消息建立一個新的header。
因為你不想為每條消息的兩個buffer都重新配置設定空間,
CompositeByteBuf
就完美适合這種情況。
下圖顯示了這個消息的布局:
下面先介紹如何通過JDK的ByteBuffer來實作這個需求:
//通過一個數組來存儲這條消息
ByteBuffer [] message = new ByteBuffer[]{header,body};
//使用副本來合并這兩個部分
ByteBuffer message2 =ByteBuffer.allocate(header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip()
這種配置設定和拷貝的方式顯然是低效且不合适的。下面介紹如何通過
CompositeByteBuf
來實作:
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = ...; //直接緩沖或堆緩沖都可
ByteBuf bodyBuf = ...; // 直接緩沖或堆緩沖都可
messageBuf.addComponents(headerBuf, bodyBuf);//将ByteBuf執行個體添加到CompositeByteBuf中
.....
messageBuf.removeComponent(0); //删除header
for (ByteBuf buf : messageBuf) {//周遊messageBuf中的ByteBuf
System.out.println(buf.toString());
}
CompositeByteBuf
可能不允許通路支援數組,是以通路
CompositeByteBuf
中的資料的方式類似于直接緩沖區模式,如下所示:
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
int length = compBuf.readableBytes();//得到可讀的位元組數
byte[] array = new byte[length];//配置設定一個位元組數組
compBuf.getBytes(compBuf.readerIndex(), array);//将資料讀到這個位元組數組中
handleArray(array, 0, array.length);
Netty通過
CompositeByteBuf
來優化socket的IO操作,盡可能的消除JDK buffer實作中的性能和記憶體使用中的不足。盡管這些優化被封裝到Netty的核心代碼中,但你應該意識到這些優化的影響。
位元組級别的操作
除了基本的讀寫操作,ByteBuf提供了大量的修改它資料的方法。下面我們會讨論最重要的一些。
随機通路索引
ByteBuf 使用從0開始的索引,第一個位元組的索引是 0,最後一個位元組的索引是 ByteBuf的capacity()- 1。下面的代碼顯示了疊代ByteBuf的内容有多簡單:
ByteBuf buffer = ...;
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i);
System.out.println((char) b);
}
順序通路索引
盡管ByteBuf有讀寫索引,而JDK的ByteBuffer隻有一個索引,這就是為什麼你需要使用
flip()
方法來切換讀寫模式。下圖顯示了ByteBuf被兩個索引分成了三個區域:
第一個是已經讀取過的位元組,是以可以被丢棄;第二個要可讀的位元組,也就是ByteBuf的内容;第三個是可添加新的位元組的區域。
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
| | (CONTENT) | |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
這和源碼中示例圖類似。
可丢棄位元組(Discardable bytes)
上圖中标記為"discardable bytes"的部分中的位元組已經被讀取過,通過調用
discardReadBytes()
方法它們能被丢棄同時它們的空間能被回收。這個部分中的初始大小為0,儲存在
readerIndex
中,随着讀操作(
read*
方法)的執行而增加。
下圖顯示了調用
discardReadBytes()
後的結果。你能發現可丢棄位元組部分的空間已經變得可用,并配置設定到可寫空間中去了。注意,在調用
discardReadBytes()
後無法保證可寫部分的内容是什麼樣的。
你可能會經常調用
discardReadBytes()
方法來增大可寫部分的空間,請注意這很有可能會導緻記憶體複制,因為可讀的位元組不得不移動到buffer的開端處。建議隻有在真正需要的時候才這麼做,比如當記憶體快要溢出(premium)的時候。
可讀位元組(Readable bytes)
可讀位元組部分存儲了實際的内容。一個新配置設定、wrap、複制的buffer的
readerIndex
是0。任何名稱以read或skip開頭的操作會檢索或跳過目前的
readerIndex
,然後增加讀取了的位元組數量。
看一下下面這個方法:
/**
* 将這個buffer(this,調用這個方法的buffer對象)的資料傳輸到這個特定的dst。以this對象的目前
* readerIndex開始直到dst變得不可寫,然後增加傳輸的位元組數量到this對象的readerIndex上。
*
* @throws IndexOutOfBoundsException 如果 dst.writableBytes > this.readableBytes
*/
public abstract ByteBuf readBytes(ByteBuf dst);
如果嘗試從一個已經沒有可讀資料的buffer中讀取資料會引發一個IndexOutOfBoundsException。
下面的代碼顯示了如何讀取所有可讀的資料:
ByteBuf buffer = ...;
while (buffer.isReadable()) {
System.out.println(buffer.readByte());
}
可寫位元組(Writable bytes)
可讀位元組部分是一個有未定義内容的可寫記憶體區域。新配置設定的buffer的
writerIndex
是0。任何名稱以write開頭的方法會從目前的
writerIndex
開始寫資料,增加剛才寫的位元組數量。
如果嘗試寫的位元組大小超過了這個buffer的容量,會引發IndexOutOfBoundException。
下面顯示了如何正确地向buffer中寫資料:
ByteBuf buffer = ...;
while (buffer.writableBytes() >= 4) {
buffer.writeInt(random.nextInt());
}
索引管理(Index management)
JDK的
InputStream
定義了mark(int readlimit)和reset()兩個方法,這些方法用來标注這個stream的目前索引到一個特定的位置,然後可以相應的将stream重置(reset())到剛才标注的位置。
類似地,你能通過調用
markReaderIndex()
,
markWriterIndex()
,
resetReaderIndex()
和
resetWriterIndex()
來設定和複位ByteBuf的
readerIndex
和
writerIndex
。除了沒有
readlimit
參數來指定什麼時候标記失效,這些很像
InputStream
的調用。
你也能通過調用
readerIndex(int)
或
writerIndex(int)
方法來将索引移到一個特定的位置。嘗試設定索引到一個無效的位置也會導緻IndexOutOfBoundsException。
可以通過調用
clear()
方法将
readerIndex
和
writerIndex
都設為0。注意這并不會導緻任何記憶體釋放。
假設在調用
clear()
方法之前一個ByteBuf執行個體有3個部分,如下:
調用之後如下:
這一部分的大小和這個ByteBuf的容量一樣大,是以所有的空間都是可寫的。
調用
clear()
的開銷沒有
discardReadBytes()
那麼大,因為它不需要任何記憶體複制。
搜尋操作(Search operations)
有幾種方法可以檢測特定值的索引。最簡單的是
indexOf()
方法,更複雜的方式是調用以
ByteBufProcessor
接口作為參數的方法。這個接口隻定義了一個方法:
boolean process(byte value)
ByteBufProcessor
中還定義了很多目标常量,假如你的應用與Flash的socket有互動的話,它有以null為結尾的内容,調用
forEachByte(ByteBufProcessor.FIND_NUL)
來處理Flash中的資料還是很簡單高效的,因為在處理過程中隻做了很少的一些邊界檢查。
通過
ByteBufProcessor
來查找
\r
int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);
衍生(Derived)Buffer
衍生Buffer為ByteBuf提供一個以特定方式呈現内容的視圖。這些視圖通過如下方法建立:
- duplicate()
- slice()
- slice(int,int)
- Unpooled.unmodifiableBuffer(…)
- order(ByteOrder)
- readSlice(int)
每個方法傳回一個新的具有自己的讀、寫和标記索引的ByteBuf執行個體。新執行個體和源執行個體(調用者寫方法的執行個體)之間共享内部存儲。這讓一個衍生Buffer可以低消耗的建立,但也意味着你修改了衍生Buffer的内容會改變源Buffer的内容(因為是共享的),反之亦然。
複制ByteBuf 如果你想真正的複制一個buffer,可通過copy()或copy(int,int)方法。
下面的代碼展示了
slice(int,int)
方法的用法:
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);//建立一個ByteBuf
ByteBuf sliced = buf.slice(0, 14);//建立這個ByteBuf的一個切片
System.out.println(sliced.toString(utf8));//輸出 Netty in Actio
buf.setByte(0, (byte)'J');
assert buf.getByte(0) == sliced.getByte(0);//成功
System.out.println(sliced.toString(utf8));//輸出 Jetty in Actio
下面看copy()方法與slice()方法的不同:
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);//建立一個ByteBuf
ByteBuf copy = buf.copy(0, 14);//建立這個ByteBuf的一個切片
System.out.println(copy.toString(utf8));//輸出 Netty in Actio
buf.setByte(0, (byte)'J');
assert buf.getByte(0) != copy.getByte(0);//成功
這兩個例子展示了修改一個切片和一個拷貝對原來ByteBuf的影響。隻要可能,推薦使用slice()方法來避免記憶體複制。
讀寫操作
Netty中有兩種讀寫操作:
- get()和set()操作以一個指定的索引開始但不會修改這個索引
- read()和write()操作以給定的索引開始并根據通路的資料大小而修改索引
下表列出了最常用的
get*
方法:
名稱 | 描述 |
setBoolean(int, boolean) | 設定Boolean值到給定索引處 |
setByte(int index, int value) | 設定byte值到給定索引處 |
setMedium(int index, int value) | 設定24位中整型(24-bit medium)值到給定索引處 |
setInt(int index, int value) | 設定int值到給定索引處 |
setLong(int index, int value) | 設定long值到給定索引處 |
setShort(int index, int value) | 設定short值到給定索引處 |
下面的代碼描述了get()和set()方法的使用,從中可以看到它們不會修改讀寫索引:
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
System.out.println((char)buf.getByte(0));//'N'
int readerIndex = buf.readerIndex();//存儲目前的讀索引
int writerIndex = buf.writerIndex();//存儲目前的寫索引
buf.setByte(0, (byte)'B');//更新值
System.out.println((char)buf.getByte(0));//'B'
assert readerIndex == buf.readerIndex();//成功
assert writerIndex == buf.writerIndex();//成功
下面來學習
read*
操作,這些方法作用于目前的讀寫索引。它們通過将ByteBuf看成stream的方式來從中讀資料,下表顯示了常用的方法:
名稱 | 描述 |
readBoolean() | 傳回目前readerIndex處的Boolean值,然後将readerIndex加1 |
readByte() | 傳回目前readerIndex處的byte值,然後将readerIndex加1 |
readUnsignedByte() | 傳回目前readerIndex處的無符号byte值并作為一個short類型,然後将readerIndex加1 |
readMedium() | 傳回目前readerIndex處的24位中整型值,然後将readerIndex加3 |
readUnsignedMedium() | 傳回目前readerIndex處的無符号24位中整型值,然後将readerIndex加3 |
readInt() | 傳回目前readerIndex處的int值,然後将readerIndex加4 |
readUnsignedInt() | 傳回目前readerIndex處的無符号int值,然後将readerIndex加4 |
readLong() | 傳回目前readerIndex處的long值,然後将readerIndex加8 |
readShort() | 傳回目前readerIndex處的short值,然後将readerIndex加2 |
readUnsignedShort() | 傳回目前readerIndex處的無符号short值,然後将readerIndex加2 |
| 将目前ByteBuf(從目前readerIndex開始)的資料傳輸到目标ByteBuf(從dstIndex開始複制)。并增加傳輸資料的大小(位元組數量)到目前ByteBuf的readerIndex中 |
幾乎每個
read*
方法都有響應的
write*
方法,注意下表列出的這些方法的參數要要寫入的值,而不是索引值。
名稱 | 描述 |
writeBoolean(boolean) | 将Boolean值寫入到目前writerIndex,然後将writerIndex加1 |
writeByte(int) | 将byte值寫入到目前writerIndex,然後将writerIndex加1 |
writeMedium(int) | 将中整型值寫入到目前writerIndex,然後将writerIndex加3 |
writeInt(int) | 将int值寫入到目前writerIndex,然後将writerIndex加4 |
writeLong(long) | 将long值寫入到目前writerIndex,然後将writerIndex加8 |
writeShort(int) | 将short值寫入到目前writerIndex,然後将writerIndex加2 |
| 将資料從特定的ByteBuf或byte數組傳輸到目前ByteBuf(從它的writerIndex開始寫)中。如果提供了srcIndex和length,那麼就會從srcIndex開始拷貝,并拷貝length大小的位元組到目前ByteBuf中。并增加傳輸資料的大小(位元組數量)到目前ByteBuf的writerIndex中 |
下面展示這些方法怎麼使用:
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
System.out.println((char)buf.readByte());//N
int readerIndex = buf.readerIndex();
int writerIndex = buf.writerIndex();
buf.writeByte((byte)'?');
System.out.println(buf.toString(utf8));//etty in Action rocks!?
assert readerIndex == buf.readerIndex();//成功
assert writerIndex != buf.writerIndex();//成功
更多的操作
下表列出了其他有用的操作:
名稱 | 描述 |
isReadable() | 如果至少還有一個位元組可被讀的話傳回true |
isWritable() | 如果至少還有一個位可寫入的話傳回true |
readableBytes() | 傳回可讀的位元組數量 |
writableBytes() | 傳回可寫的位元組數量 |
capacity() | 傳回目前ByteBuf能存儲的位元組數量 |
maxCapacity() | 傳回ByteBuf最多能存儲多少位元組數量 |
hasArray() | 傳回Byte是否有支援數組 |
array() | 如果有支援數組,傳回;否則抛UnsupportedOperationException |
ByteBufHolder接口
我們經常會遇到除了需要存儲實際的資料内容還需要存儲各種各樣的屬性值的情況,比如HTTP響應,除了以位元組表示的内容還會有狀态碼、cookie等等。
Netty提供了ByteBufHolder接口來滿足這種用例。ByteBufHolder也支援Netty的進階特性,如buffer池,可以從buffer池中取一個可用的ByteBuf,如果需要還可以自動釋放。
ByteBufHolder隻有幾個方法來通路底層的ByteBuf和引用計數,下表中列出了這些方法:
名稱 | 描述 |
content() | 傳回這個ByteBufHolder持有的ByteBuf |
copy() | 傳回這個ByteBufHolder的一個深複制,包括一個它含有的ByteBuf資料的一個拷貝(非共享的) |
duplicate() | 傳回這個ByteBufHolder的一個淺複制,包括一個它含有的ByteBuf資料的一個拷貝(共享的) |
如果你想實作一個“消息對象”有效負載存儲在ByteBuf,使用ByteBufHolder是一個好主意。
ByteBuf記憶體配置設定(ByteBuf allocation)
這一節我們會介紹管理ByteBuf執行個體的方法
請求式:ByteBufAllocator接口
為了減少配置設定和釋放記憶體的總開銷,Netty通過ByteBufAllocator實作ByteBuf池,ByteBufAllocator負責配置設定ByteBuf執行個體。
下表列出了ByteBufAllocator提供的方法:
名稱 | 描述 |
buffer() | 傳回一個基于直接緩沖區或堆緩沖區的ByteBuf |
heapBuffer() | 傳回一個基于堆緩沖區的ByteBuf |
directBuffer() | 傳回一個基于直接緩沖區的ByteBuf |
ioBuffer() | 傳回一個适用于socket上的IO操作的ByteBuf(一般是直接緩沖區的) |
你可以從一個Channel或與ChannelHandler綁定的ChannelHandlerContext中獲得一個ByteBufAllocator的引用。
下面展示了這種方式的用法:
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();
Netty為ByteBufAllocator提供了兩種實作: PooledByteBufAllocator和UnpooledByteBufAllocator。前者通過ByteBuf的執行個體放入池中來提高性能同時減少記憶體碎片。後者每次傳回一個建立的執行個體而沒有通過池來緩存。
盡管Netty預設使用PooledByteBufAllocator,但是可以在啟動你應用的時候通過 ChannelConfig 的 API來修改。
Unpooled buffers
可能你無法獲得ByteBufAllocator的引用,在這種情況下,Netty提供了一個叫做
Unpooled
的工具類,它提供了靜态的方法來建立不從池中擷取的(unpooled)建立的ByteBuf執行個體。
下表列出了最重要的一些方法:
名稱 | 描述 |
buffer() | 傳回一個基于堆緩沖區的ByteBuf |
directBuffer() | 傳回一個基于直接緩沖區的ByteBuf |
wrappedBuffer() | 傳回一個包裝了(wrap)給定資料的ByteBuf |
copiedBuffer() | 傳回一個持有給定資料的一個拷貝的ByteBuf |
Unpooled
類使在非網絡程式設計項目中也可以用到ByteBuf。
ByteBufUtil類
ByteBufUtil提供了用于操縱ByteBuf的靜态幫助方法。因為這API是通用的,與池無關,是以這些方法已經在配置設定記憶體的類之外實作了。
這些靜态方法中最重要的可能是
hexdump()
了,它能以十六進制的形式列印一個ByteBuf執行個體的内容。這在日志記錄中十分有用;另一個有用的方法是
equals(ByteBuf,ByteBuf)
方法,它傳回這兩個ByteBuf是否相等(以一定的規則判斷,比如可讀位元組數大小、
a[aStartIndex : aStartIndex + length] == b[bStartIndex : bStartIndex + length]
其中 a[i:k]表示a[i]、a[i+1]、a[i+2]…a[k-1])。
引用計數
引用計數是一種優化記憶體使用和性能的技術,當對象不再被引用時,釋放對象所持有的資源。Netty從4.0版本開始為ByteBuf和ByteBufHolder引入了引用計數,它們都實作了
ReferenceCounted
接口。
引用計數背後的原理不是很複雜,主要是跟蹤有多少個活躍引用指向了某個對象。隻要某個對象的引用計數大于0,可以保證這個對象不會被釋放。當某個對象的引用計數變成0時,這個對象将會被釋放。
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ByteBuf buffer = allocator.directBuffer();
assert buffer.refCnt() == 1;//檢測buffer的引用計數是否為1
ByteBuf buffer = ...;
//減少這個對象的引用計數,如果減少到0,這個對象将會被釋放
//,并且這個方法傳回true。
boolean released = buffer.release();