在Netty中,還有另外一個比較常見的物件ByteBuf,它其實等同于Java Nio中的ByteBuffer,但是ByteBuf對Nio中的ByteBuffer的功能做了很作增強,下面我們來簡單了解一下ByteBuf,
下面這段代碼演示了ByteBuf的創建以及內容的列印,這里顯示出了和普通ByteBuffer最大的區別之一,就是ByteBuf可以自動擴容,默認長度是256,如果內容長度超過閾值時,會自動觸發擴容
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可自動擴容
log(buf);
StringBuilder sb=new StringBuilder();
for (int i = 0; i < 32; i++) { //演示的時候,可以把回圈的值擴大,就能看到擴容效果
sb.append(" - "+i);
}
buf.writeBytes(sb.toString().getBytes());
log(buf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex()) //獲取讀索引
.append(" write index:").append(buf.writerIndex()) //獲取寫索引
.append(" capacity:").append(buf.capacity()) //獲取容量
.append(StringUtil.NEWLINE);
//把ByteBuf中的內容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
ByteBuf創建的方法有兩種
-
第一種,創建基于堆記憶體的ByteBuf
ByteBuf buffer=ByteBufAllocator.DEFAULT.heapBuffer(10); -
第二種,創建基于直接記憶體(堆外記憶體)的ByteBuf(默認情況下用的是這種)
Java中的記憶體分為兩個部分,一部分是不需要jvm管理的直接記憶體,也被稱為堆外記憶體,堆外記憶體就是把記憶體物件分配在JVM堆意外的記憶體區域,這部分記憶體不是虛擬機管理,而是由作業系統來管理,這樣可以減少垃圾回收對應用程式的影響
ByteBufAllocator.DEFAULT.directBuffer(10);直接記憶體的好處是讀寫性能會高一些,如果資料存放在堆中,此時需要把Java堆空間的資料發送到遠程服務器,首先需要把堆內部的資料拷貝到直接記憶體(堆外記憶體),然后再發送,如果是把資料直接存盤到堆外記憶體中,發送的時候就少了一個復制步驟,
但是它也有缺點,由于缺少了JVM的記憶體管理,所以需要我們自己來維護堆外記憶體,防止記憶體溢位,
另外,需要注意的是,ByteBuf默認采用了池化技術來創建,關于池化技術在前面的課程中已經重復講過,它的核心思想是實作物件的復用,從而減少物件頻繁創建銷毀帶來的性能開銷,
池化功能是否開啟,可以通過下面的環境變數來控制,其中unpooled表示不開啟,
-Dio.netty.allocator.type={unpooled|pooled}
public class NettyByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
System.out.println(buf);
}
}
ByteBuf的存盤結構
ByteBuf的存盤結構如圖3-1所示,從這個圖中可以看到ByteBuf其實是一個位元組容器,該容器中包含三個部分
- 已經丟棄的位元組,這部分資料是無效的
- 可讀位元組,這部分資料是ByteBuf的主體資料,從ByteBuf里面讀取的資料都來自這部分; 可寫位元組,所有寫到ByteBuf的資料都會存盤到這一段
- 可擴容位元組,表示ByteBuf最多還能擴容多少容量,

在ByteBuf中,有兩個指標
- readerIndex: 讀指標,每讀取一個位元組,readerIndex自增加1,ByteBuf里面總共有witeIndex-readerIndex個位元組可讀,當readerIndex和writeIndex相等的時候,ByteBuf不可讀
- writeIndex: 寫指標,每寫入一個位元組,writeIndex自增加1,直到增加到capacity后,可以觸發擴容后繼續寫入,
- ByteBuf中還有一個maxCapacity最大容量,默認的值是
Integer.MAX_VALUE,當ByteBuf寫入資料時,如果容量不足時,會觸發擴容,直到capacity擴容到maxCapacity,
ByteBuf中常用的方法
對于ByteBuf來說,常見的方法就是寫入和讀取
Write相關方法
對于write方法來說,ByteBuf提供了針對各種不同資料型別的寫入,比如
- writeChar,寫入char型別
- writeInt,寫入int型別
- writeFloat,寫入float型別
- writeBytes, 寫入nio的ByteBuffer
- writeCharSequence, 寫入字串
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自動擴容
buf.writeBytes(new byte[]{1,2,3,4}); //寫入四個位元組
log(buf);
buf.writeInt(5); //寫入一個int型別,也是4個位元組
log(buf);
}
private static void log(ByteBuf buf){
System.out.println(buf);
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的內容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
擴容
當向ByteBuf寫入資料時,發現容量不足時,會觸發擴容,而具體的擴容規則是
假設ByteBuf初始容量是10,
- 如果寫入后資料大小未超過512個位元組,則選擇下一個16的整數倍進行庫容, 比如寫入資料后大小為12,則擴容后的capacity是16,
- 如果寫入后資料大小超過512個位元組,則選擇下一個2n, 比如寫入后大小是512位元組,則擴容后的capacity是210=1024 ,(因為29=512,長度已經不夠了)
- 擴容不能超過max capacity,否則會報錯,
Reader相關方法
reader方法也同樣針對不同資料型別提供了不同的操作方法,
- readByte ,讀取單個位元組
- readInt , 讀取一個int型別
- readFloat ,讀取一個float型別
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自動擴容
buf.writeBytes(new byte[]{1,2,3,4});
log(buf);
System.out.println(buf.readByte());
log(buf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的內容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
從下面結果中可以看到,讀完一個位元組后,這個位元組就變成了廢棄部分,再次讀取的時候只能讀取 未讀取的部分資料,
read index:0 write index:7 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 |....... |
+--------+-------------------------------------------------+----------------+
1
read index:1 write index:7 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 02 03 04 05 06 07 |...... |
+--------+-------------------------------------------------+----------------+
Process finished with exit code 0
另外,如果想重復讀取哪些已經讀完的資料,這里提供了兩個方法來實作標記和重置
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自動擴容
buf.writeBytes(new byte[]{1,2,3,4,5,6,7});
log(buf);
buf.markReaderIndex(); //標記讀取的索引位置
System.out.println(buf.readInt());
log(buf);
buf.resetReaderIndex();//重置到標記位
System.out.println(buf.readInt());
log(buf);
}
另外,如果想不改變讀指標位置來獲得資料,在ByteBuf中提供了get開頭的方法,這個方法基于索引位置讀取,并且允許重復讀取的功能,
ByteBuf的零拷貝機制
需要說明一下,ByteBuf的零拷貝機制和我們之前提到的作業系統層面的零拷貝不同,作業系統層面的零拷貝,是我們要把一個檔案發送到遠程服務器時,需要從內核空間拷貝到用戶空間,再從用戶空間拷貝到內核空間的網卡緩沖區發送,導致拷貝次數增加,
而ByteBuf中的零拷貝思想也是相同,都是減少資料復制提升性能,如圖3-2所示,假設有一個原始ByteBuf,我們想對這個ByteBuf其中的兩個部分的資料進行操作,按照正常的思路,我們會創建兩個新的ByteBuf,然后把原始ByteBuf中的部分資料拷貝到兩個新的ByteBuf中,但是這種會涉及到資料拷貝,在并發量較大的情況下,會影響到性能,

ByteBuf中提供了一個slice方法,這個方法可以在不做資料拷貝的情況下對原始ByteBuf進行拆分,使用方法如下
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可自動擴容
buf.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,10});
log(buf);
ByteBuf bb1=buf.slice(0,5);
ByteBuf bb2=buf.slice(5,5);
log(bb1);
log(bb2);
System.out.println("修改原始資料");
buf.setByte(2, 5); //修改原始buf資料
log(bb1);//再列印bb1的結果,發現資料發生了變化
}
在上面的代碼中,通過slice對原始buf進行切片,每個分片是5個位元組,
為了證明slice是沒有資料拷貝,我們通過修改原始buf的索引2所在的值,然后再列印第一個分片bb1,可以發現bb1的結果發生了變化,說明兩個分片和原始buf指向的資料是同一個,
Unpooled
在前面的案例中我們經常用到Unpooled工具類,它是同了非池化的ByteBuf的創建、組合、復制等操作,
假設有一個協議資料,它有頭部和訊息體組成,這兩個部分分別放在兩個ByteBuf中
ByteBuf header=...
ByteBuf body= ...
我們希望把header和body合并成一個ByteBuf,通常的做法是
ByteBuf allBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
在這個程序中,我們把header和body拷貝到了新的allBuf中,這個程序在無形中增加了兩次資料拷貝操作,那有沒有更高效的方法減少拷貝次數來達到相同目的呢?
在Netty中,提供了一個CompositeByteBuf組件,它提供了這個功能,
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自動擴容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();
//其中第一個引數是 true, 表示當添加新的 ByteBuf 時, 自動遞增 CompositeByteBuf 的 writeIndex.
//默認是false,也就是writeIndex=0,這樣的話我們不可能從compositeByteBuf中讀取到資料,
compositeByteBuf.addComponents(true,header,body);
log(compositeByteBuf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的內容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
之所以CompositeByteBuf能夠實作零拷貝,是因為在組合header和body時,并沒有對這兩個資料進行復制,而是通過CompositeByteBuf構建了一個邏輯整體,里面仍然是兩個真實物件,也就是有一個指標指向了同一個物件,所以這里類似于淺拷貝的實作,

wrappedBuffer
在Unpooled工具類中,提供了一個wrappedBuffer方法,來實作CompositeByteBuf零拷貝功能,使用方法如下,
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自動擴容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
ByteBuf allBb=Unpooled.wrappedBuffer(header,body);
log(allBb);
//對于零拷貝機制,修改原始ByteBuf中的值,會影響到allBb
header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8);
log(allBb);
}
copiedBuffer
copiedBuffer,和wrappedBuffer最大的區別是,該方法會實作資料復制,下面代碼演示了copiedBuffer和wrappedbuffer的區別,可以看到在case標注的位置中,修改了原始ByteBuf的值,并沒有影響到allBb,
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自動擴容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
ByteBuf allBb=Unpooled.copiedBuffer(header,body);
log(allBb);
header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8); //case
log(allBb);
}
記憶體釋放
針對不同的ByteBuf創建,記憶體釋放的方法不同,
- UnpooledHeapByteBuf,使用JVM記憶體,只需要等待GC回收即可
- UnpooledDirectByteBuf,使用對外記憶體,需要特殊方法來回收記憶體
- PooledByteBuf和它的之類使用了池化機制,需要更復雜的規則來回收記憶體
如果ByteBuf是使用堆外記憶體來創建,那么盡量手動釋放記憶體,那怎么釋放呢?
Netty采用了參考計數方法來控制記憶體回收,每個ByteBuf都實作了ReferenceCounted介面,
- 每個ByteBuf物件的初始計數為1
- 呼叫release方法時,計數器減一,如果計數器為0,ByteBuf被回收
- 呼叫retain方法時,計數器加一,表示呼叫者沒用完之前,其他handler即時呼叫了release也不會造成回收,
- 當計數器為0時,底層記憶體會被回收,這時即使ByteBuf物件還存在,但是它的各個方法都無法正常使用
著作權宣告:本博客所有文章除特別宣告外,均采用 CC BY-NC-SA 4.0 許可協議,轉載請注明來自
Mic帶你學架構!
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力,歡迎關注「跟著Mic學架構」公眾號公眾號獲取更多技術干貨!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/356648.html
標籤:Java
下一篇:M1配置php環境完整版(用于M1芯片的Mac中,php開發環境,比如wordpress、"或wp"、emlog pro、typecho等本地開發環境的配置)
