Netty基礎必備知識,ByteBuffer和ByteBuf底層原理

      網友投稿 894 2022-05-29

      前言

      本文章只討論ByteBuffer和ByteBuf的底層結構的區別,如果想要了解堆內內存和堆外內存的區別,請看我的另一篇文章:java堆外內存詳解(又名直接內存)和ByteBuffer

      什么是Buffer

      中文稱為緩沖區,指的是從網絡或者文件讀寫數據的時候,在他們中間多了個緩沖區,應用程序只需要對著緩沖區 進行讀寫即可;然后緩沖區在將數據復制到內核或者從內核讀取數據;這種方式加快讀寫速度,減少了IO次數;小文件的讀寫用不用緩沖區速度都沒有多大區別,但是當我們進行大文件進行讀寫的時候一般都會使用到緩沖區;讀寫效率會以倍數增長;

      為什么需要Buffer

      在我們剛學習IO的時候,寫入文件都是使用FileInputStream或者FileOutputStream類來讀取/寫入,但是這種方式是你每調用一次write()或者read()方法都是直接將數據寫到到內核中,再由內核復制到磁盤中,每次都需要在內核態和用戶態頻繁切換,這些切換的工作都是需要系統資源開銷的,特別是切換太頻繁的話,讀寫效率就會下降;所以這邊會推薦大家使用BufferedOutputStream,當緩沖區的數據大小到達8KB時才會寫入文件;

      ByteBuffer

      當我們在文件或者網絡進行數據傳輸的時候,往往需要使用到緩沖區,常用的緩沖區就是JDK NIO類庫提供的java.nio.Buffer;基本上每個基本的數據類型都有緩沖區(Boolean除外)

      java.nio.ByteBuffer; java.nio.CharBuffer; java.nio.DoubleBuffer; java.nio.FloatBuffer; java.nio.IntBuffer; java.nio.LongBuffer; java.nio.ShortBuffer;

      1

      2

      3

      4

      5

      6

      7

      一般來說,ByteBuffer 就已經能夠滿足IO的編程需要了,ByteBuffer 是java NIO(new IO)自帶的類,主要有以下特點:

      長度一旦設定,不可擴容或收縮,要擴容只能創建一個新的ByteBuffer 對象;

      ByteBuffer 內部有一個指針位置position,通過移動指針可實現靈活的讀寫功能,讀寫時可通過調用flip()方法進行翻轉指針位置;

      支持堆內和堆外分配;

      使用者必須小心謹慎地處理這些API,否則很容易導致程序處理失敗;

      ByteBuffer 內部結構

      ByteBuffer 內部有一個byte[]數組,我們添加進去的字節就是加入到這個數組里面的,除此之外,內部還維護了4個指針

      position :默認為0;當前下標的位置,表示下一個讀/寫的起始位置,每寫一個字節 或者每讀一個字節 position就 + 1;

      capacity:緩沖區大小,也就是數組的大小,一旦指定,不可修改;

      limit:結束標記位置,表示進行下一個讀寫操作時的結束位置;

      mark : 用戶可通過調用mark()方法標記position的當前位置,標記后,在后面的讀寫發生問題時可通過調用reset() 方法回退到標記位置;

      代碼示例

      @Test public void main() { // 如果添加的元素超過buffer大小,會拋出BufferOverflowException異常 ByteBuffer buffer = ByteBuffer.allocate(10); showPosition(buffer); // 將2個字節的數據寫入緩沖區 buffer.put((byte) 34); buffer.put((byte) 78); showPosition(buffer); buffer.flip();// 翻轉后可進行讀取 //初始化字節數組,用來讀取內存 byte[] bytes = new byte[buffer.limit()]; // 進行讀取 buffer.get(bytes); // 講讀取到的內容打印出來 System.out.println(Arrays.toString(bytes)); showPosition(buffer); // 清除緩沖區,此方法并不是直接清楚buffer內的數組內容,而是將position和limit復位 buffer.clear(); showPosition(buffer); } // 顯示位置 public void showPosition(ByteBuffer buffer) { // position 默認為0;當前下標的位置,表示下一個讀/寫的起始位置,每寫一個字節 position就+1; System.out.println("position 當前位置:" + buffer.position()); // capacity 緩沖區的大小,一旦指定,不可修改; System.out.println("capacity 緩沖區大小:" + buffer.capacity()); // limit 結束標記位置,表示進行下一個讀寫操作時的結束位置; System.out.println("limit 結束標記位置:" + buffer.limit()); try { // 打印mark 標記位置,mark在Buffer抽象類中,且是私有屬性,所以通過反射獲取 Field mark = Buffer.class.getDeclaredField("mark"); mark.setAccessible(true); System.out.println("mark 標記位置:" + mark.get(buffer)); } catch (Exception e) { e.printStackTrace(); } System.out.println(); }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      運行后,打印結果如下,這邊就可以看到每走一步后具體的位置下標了,mark標記的值為-1,是因為在代碼中并沒有調用mark()進行標記了所以為-1;

      position 當前位置:0 capacity 緩沖區大小:10 limit 結束標記位置:10 mark 標記位置:-1 position 當前位置:2 capacity 緩沖區大小:10 limit 結束標記位置:10 mark 標記位置:-1 [34, 78] position 當前位置:2 capacity 緩沖區大小:10 limit 結束標記位置:2 mark 標記位置:-1 position 當前位置:0 capacity 緩沖區大小:10 limit 結束標記位置:10 mark 標記位置:-1

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      什么?看不懂? 沒關系,我畫圖給你看,走了每一行代碼之后內部結構的變化

      創建一個堆內內存的ByteBuffer 實例,緩沖區大小為10,此時數組內還沒有數據,position 指針在0的位置,所以目前數組內的數據都為0;

      在這一環節中往緩沖區寫入了2個字節;

      buffer.put((byte) 34); buffer.put((byte) 78);

      1

      2

      寫完后,position向右移動了2個位置,表示寫到了某位置,下次寫一個字節時就會往當前的position位置上寫入;

      如果需要進行讀取了,就可以調用翻轉方法,翻轉后,position的位置又回到了第一個位置,并且limit結束符也到了第2個位置(從0開始算),

      需要注意的是:如果現在讀取或者寫入超過了2個字節,將會拋出異常:BufferOverflowException,因為不管在任何情況下,都不能寫入或讀取超過(limit - position)個字節

      此時position的位置已經在第一個上面了,所以讀取也是從第一個進行讀取的,

      注意:如果現在寫入新的字節,將會覆蓋之前寫入的數據;

      byte[] bytes = new byte[buffer.limit()]; buffer.get(bytes); System.out.println(Arrays.toString(bytes));

      1

      2

      3

      清除緩沖區,clear()方法并不是直接清楚buffer內的數組內容,而是將position和limit復位,position會回到0的位置,limit也會回到數組末尾位置;剛剛加入的數據還是存在數組內部的;

      拷貝 duplicate()

      內部還提供了一個方法可以講緩沖區進行拷貝,但是這個拷貝后內部的數組和源對象的數組其實是共享的,只是重新包裝了一下,也就是位置變量(position、limit)不同而已,

      ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put((byte) 34); buffer.put((byte) 78); // 拷貝 ByteBuffer duplicate = buffer.duplicate(); buffer.put((byte) 77); buffer.put((byte) 77);

      1

      2

      3

      4

      5

      6

      7

      執行后看下圖,兩個數組的地址是一樣的;

      flip()和rewind()的區別

      看源碼就可以得知,flip()只是多了一個結束位的配置,因為limit是限制位,也就是說調用了flip()后可以寫入或者讀取的數據是根據當前的position來決定的,而rewind()方法則可以寫完或者讀完數組中的所有內容;

      public final Buffer flip() { limit = position; position = 0; mark = -1; return this; } public final Buffer rewind() { position = 0; mark = -1; return this; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      ByteBuf

      Netty基礎必備知識,ByteBuffer和ByteBuf底層原理

      ByteBuf是Netty通過ByteBuffer的原理自己封裝的一個類,使用時必須先加入netty依賴才可使用;

      io.netty netty-all 4.1.49.Final

      1

      2

      3

      4

      5

      ByteBuf 和 ByteBuffer的區別

      和ByteBuffer最大的區別就是ByteBuf的讀寫指針是分開的,也就是說ByteBuf內部有一個讀指針(readerIndex)和一個寫指針(writerIndex),因此讀寫時不需要翻轉指針;而ByteBuffer只有一個position指針,讀寫需要調用flip()或者rewind()方法進行翻轉;

      和ByteBuffer一樣,ByteBuf也支持堆內內存和直接內存的分配,且直接內存都是用Unsafe類實現的;

      和ByteBuffer最大的不同,就是ByteBuf支持內存池,了解過數據庫連接池和線程池的童鞋肯定不陌生,內存池的設計可以加快效率和提高減少資源消耗;

      初始化ByteBuf

      實例化ByteBuf有四種方式,分別是

      堆內非池化

      堆內池化

      堆外非池化

      堆外池化

      在java代碼種實例化方式如下

      // 堆內非池化 public ByteBuf heapInnerUnpool(){ return UnpooledByteBufAllocator.DEFAULT.heapBuffer(10,100); } // 堆內池化 public ByteBuf heapInnerPool(){ return PooledByteBufAllocator.DEFAULT.heapBuffer(10,100); } //堆外非池化 public ByteBuf heapOutUnpool(){ return UnpooledByteBufAllocator.DEFAULT.buffer(10,100); } //堆外池化 public ByteBuf heapOutPool(){ return PooledByteBufAllocator.DEFAULT.buffer(10,100); }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      ByteBuf 內存池

      什么是內存池

      從netty 4開始,netty加入了內存池管理,采用內存池管理比普通的ByteBuf性能提高了數十倍;這也是為什么netty快的原因,ByteBuf 支持2種模式,池化和非池化, 池化就是使用內存池,非池化就是不使用內存池,這個很好理解。

      為什么要使用內存池

      在未使用池化之前,每次創建一個ByteBuf 都都需要先向操作系統申請一塊內存,并且為這個對象進行實例化 → 初始化→引用賦值;這些過程都是需要消耗CPU資源的;

      將ByteBuf池化之后,只有第首次創建對象會進行實例化 → 初始化→引用賦值,默認大小16MB,以后使用的時候就直接使用首次創建的對象就可以了;

      驗證內存池

      現在我們來做一個試驗,創建2個池化的ByteBuf對象,看看內部是否使用同一塊內存空間

      ByteBuf byteBuf_one = PooledByteBufAllocator.DEFAULT.heapBuffer(10, 20); ByteBuf byteBuf_two = PooledByteBufAllocator.DEFAULT.heapBuffer(20, 40);

      1

      2

      在idea上使用debug功能后發現,在memory這個屬性里面存放就是byte[]數組,而byteBuf_one和byteBuf_two 使用的內存地址都是相同的,這足以證明它們使用的是同一塊內存地址;

      除此之外,在上圖種我們還看到一個offset的屬性,這個屬性就是偏移量,在一個內存池中默認給每個ByteBuf 分配了8192byte的空間,也就是說內存池中0 - 8191 是分配給 byteBuf_one的,而 8192 - 16383 是分配給byteBuf_two的;

      讀寫示例

      我們將測試以下代碼,并且畫出內部結構圖,并且分析每一行代碼的走向,準備好了嗎?

      @Test public void test(){ // 使用內存池 ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer(10,10); print(buffer); buffer.writeBytes(new byte[]{1,2,3,4,5}); print(buffer); // 讀取2個字節 byte[] bytes = new byte[2]; buffer.readBytes(bytes, buffer.readerIndex(), 2); System.out.println(Arrays.toString(bytes)); print(buffer); // 丟棄已讀字節; buffer.discardReadBytes(); print(buffer); // 設置讀取位置,從0開始,相當于設置ByteBUffer的position值 buffer.readerIndex(2); print(buffer); // 釋放內存空間 buffer.release(); } //打印 ByteBuf 信息 public void print(ByteBuf buf){ System.out.println("默認大小:"+buf.capacity()); System.out.println("最大值:"+buf.maxCapacity()); System.out.println("是否可讀:"+buf.isReadable()); System.out.println("可讀的字節數:"+buf.readableBytes()); System.out.println("讀的位置:"+buf.readerIndex()); System.out.println("是否可寫:"+buf.isWritable()); System.out.println("可寫字節的字節數:"+buf.writableBytes()); System.out.println("寫的位置:"+buf.writerIndex()); System.out.println("是否堆外分配:"+buf.isDirect()); System.out.println("-------------------------"); }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      打印結果如下

      默認大小:10 最大值:10 是否可讀:false 可讀的字節數:0 讀的位置:0 是否可寫:true 可寫字節的字節數:10 寫的位置:0 是否堆外分配:true ------------------------- 默認大小:10 最大值:10 是否可讀:true 可讀的字節數:5 讀的位置:0 是否可寫:true 可寫字節的字節數:5 寫的位置:5 是否堆外分配:true ------------------------- [1, 2] 默認大小:10 最大值:10 是否可讀:true 可讀的字節數:3 讀的位置:2 是否可寫:true 可寫字節的字節數:5 寫的位置:5 是否堆外分配:true ------------------------- 默認大小:10 最大值:10 是否可讀:true 可讀的字節數:3 讀的位置:0 是否可寫:true 可寫字節的字節數:7 寫的位置:3 是否堆外分配:true ------------------------- 默認大小:10 最大值:10 是否可讀:true 可讀的字節數:1 讀的位置:2 是否可寫:true 可寫字節的字節數:7 寫的位置:3 是否堆外分配:true -------------------------

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      44

      45

      46

      47

      48

      49

      50

      51

      接下來我們開始分析ByteBuf內部結構走向

      因為我們用到了PooledByteBufAllocator,所以這里使用的是內存池;效率更快,這行代碼是實例化了ByteBuf,創一個堆外分配的對象;雖然我們只用到了10個字節,但是內存池給這個實例分配了8192byte的字節空間;所以 0 ~ 8191 的字節是給ByteBuf 占用了的;

      這行代碼很簡單,就是往緩沖區寫入了5個字節,寫入后,結構如下

      讀取2個字節內容,并打印出來;這邊讀取到的內容為 1和2,也就是前2個元素

      byte[] bytes = new byte[2]; // 將讀取到的內容放入bytes,第二個參數是讀取的起始位置,第三個參數是你需要讀取幾個字節的數據;注意不要超過最大容量; buffer.readBytes(bytes, buffer.readerIndex(), 2); System.out.println(Arrays.toString(bytes));

      1

      2

      3

      4

      這個方法會將已讀的字節刪除,過程中需要的開銷應該會比較大,基于數組的特性,插入刪除比較慢,因為得需要移動比較多的元素指針,刪除后結構如下圖:

      這種方法相當于設置ByteBUffer的position值,這邊將讀取位置指向了2的位置,所以2之前的位置就會被認為是已經讀取過了;

      因為是內存池堆外分配的,所以每次用完之后都需要手動釋放,釋放后,內部的memory數組就是空的了,表示已經被釋放成功了,這時候這個變量就不能在使用了,會等待垃圾回收將其清理;

      動態擴容

      ByteBuf 在實例化時有2個參數,初始容量(initialCapacity)和 最大容量(maxCapacity),也就是說,實例化后,緩沖區的容量就是10,當你寫入的字節數超過10個時(比如11)就會進行擴容;

      int initialCapacity = 10; int maxCapacity = 20; UnpooledByteBufAllocator.DEFAULT.heapBuffer(initialCapacity ,maxCapacity );

      1

      2

      3

      如何擴容?

      知道ByteBuf會擴容,那它是什么時候進行擴容呢?每次擴多少呢?其實啊,ByteBuf 沒有負載因子一說,只有當容量不足時才會擴容;如果你的容量為10,而你寫入的字節數也是10,那么這種情況不會進行擴容,當你的字節數到達11個時才會擴容;如果你的最大容量是20,那么它就會擴到20;

      如果我的最大容量有511呢?

      當容量不足64時,會擴容到64,以后開始從64字節每次增加2倍,以下面的代碼為例

      ByteBuf byteBuf = UnpooledByteBufAllocator.DEFAULT.heapBuffer(10, 511); System.out.println("初始容量:"+byteBuf.capacity() + ",當前已寫入字節數:"+byteBuf.writerIndex()); byteBuf.writeBytes( new byte[64]); System.out.println("第一次擴容,寫入64字節 ,當前容量:"+byteBuf.capacity() + ",當前已寫入字節數: "+byteBuf.writerIndex()); byteBuf.writeBytes( new byte[64]); System.out.println("第二次擴容,寫入64字節 ,當前容量:"+byteBuf.capacity() + ",當前已寫入字節數:"+byteBuf.writerIndex()); byteBuf.writeBytes( new byte[128]); System.out.println("第三次擴容,寫入128字節,當前容量:"+byteBuf.capacity() + ",當前已寫入字節數:"+byteBuf.writerIndex()); byteBuf.writeBytes( new byte[1]); System.out.println("第四次擴容,寫入1字節 ,當前容量:"+byteBuf.capacity() + ",當前已寫入字節數:"+byteBuf.writerIndex());

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      打印結果如下

      初始容量:10,當前已寫入字節數:0 第一次擴容,寫入64字節 ,當前容量:64,當前已寫入字節數: 64 第二次擴容,寫入64字節 ,當前容量:128,當前已寫入字節數:128 第三次擴容,寫入128字節,當前容量:256,當前已寫入字節數:256 第四次擴容,寫入1字節 ,當前容量:511,當前已寫入字節數:257

      1

      2

      3

      4

      5

      擴容時序圖如下

      mark標記和回退

      ByteBuffer和ByteBuf都支持標記,只是用法不同而已,進行標記后,不管你下一步是讀還是寫,執行reset()方法后都能回到標記位置;

      ByteBuffer標記

      ByteBuffer buffer = ByteBuffer.allocate(10); // 將數據寫入緩沖區 buffer.put((byte) 34); buffer.put((byte) 78); // 標記當前位置 buffer.mark(); // 繼續寫入 buffer.put((byte) 96); // 回退到標記位置 buffer.reset();

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      ByteBuf標記

      ByteBuf byteBuf = UnpooledByteBufAllocator.DEFAULT.heapBuffer(10, 511); // 標記讀的位置 byteBuf.markReaderIndex(); // 標記寫的位置 byteBuf.markWriterIndex(); // 回退到讀的標記位置 byteBuf.resetReaderIndex(); //回退到寫的標記位置 byteBuf.resetWriterIndex();

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      Java 數據結構

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:計算機科學家眼中的Python程序之道
      下一篇:Android 高級繪圖
      相關文章
      久久久久久久久无码精品亚洲日韩| 久久久亚洲欧洲日产国码二区| 亚洲欧洲校园自拍都市| 亚洲国产精品久久久久| 亚洲AV成人精品网站在线播放| 久热综合在线亚洲精品| 亚洲av无码精品网站| 亚洲国产精品热久久| 18gay台湾男同亚洲男同| 亚洲视频免费一区| 亚洲精品动漫在线| 亚洲综合色区中文字幕| 久久乐国产综合亚洲精品| 最新国产精品亚洲| 亚洲大码熟女在线观看| 亚洲av色香蕉一区二区三区| 精品久久久久亚洲| 亚洲毛片av日韩av无码| 久久久久亚洲精品中文字幕| 亚洲色中文字幕无码AV| 亚洲成熟xxxxx电影| 亚洲色图综合网站| 亚洲综合校园春色| 亚洲丰满熟女一区二区哦| 国产成人综合亚洲绿色| 亚洲中文字幕无码专区| 亚洲午夜国产精品无码 | 亚洲AV综合色区无码一区爱AV| 亚洲av之男人的天堂网站| 久久亚洲精品国产精品| 亚洲国产成人久久99精品| 中文字幕无码精品亚洲资源网久久 | 亚洲字幕AV一区二区三区四区| 亚洲经典千人经典日产| 亚洲国产午夜福利在线播放| 亚洲午夜福利717| 久久亚洲AV成人出白浆无码国产 | 国产成人毛片亚洲精品| 亚洲成AV人片在线观看无| 亚洲视频欧洲视频| 亚洲一线产区二线产区区|