天天看點

BAT面試必問細節:關于Netty中的ByteBuf詳解

在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最多還能擴容多少容量。
BAT面試必問細節:關于Netty中的ByteBuf詳解

圖3-1

在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中,但是這種會涉及到資料拷貝,在并發量較大的情況下,會影響到性能。

BAT面試必問細節:關于Netty中的ByteBuf詳解

圖3-2

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建構了一個邏輯整體,裡面仍然是兩個真實對象,也就是有一個指針指向了同一個對象,是以這裡類似于淺拷貝的實作。

BAT面試必問細節:關于Netty中的ByteBuf詳解

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學架構」公衆号公衆号擷取更多技術幹貨!

BAT面試必問細節:關于Netty中的ByteBuf詳解