Netty基礎必備知識,ByteBuffer和ByteBuf底層原理
前言
本文章只討論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
ByteBuf是Netty通過ByteBuffer的原理自己封裝的一個類,使用時必須先加入netty依賴才可使用;
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小時內刪除侵權內容。