Netty核心功能之數(shù)據(jù)容器ByteBuf詳解
正文
網(wǎng)絡(luò)數(shù)據(jù)的基本單位總是字節(jié),Java NIO 提供了 ByteBuffer 作為它的字節(jié)容器,但是這個類使用起來過于復(fù)雜,而且也有些繁瑣。 Netty 的 ByteBuffer 替代品是 ByteBuf,一個強大的實現(xiàn),既解決了 JDK API 的局限性,又為網(wǎng)絡(luò)應(yīng)用程序的開發(fā)者提供了更好的 API。
1、簡介
Netty 的數(shù)據(jù)處理 API 通過兩個組件暴露——abstract class ByteBuf
和 interface ByteBufHolder
,下面是一些 ByteBuf API 的優(yōu)點:
- 它可以被用戶自定義的緩沖區(qū)類型擴展;
- 通過內(nèi)置的復(fù)合緩沖區(qū)類型實現(xiàn)了透明的零拷貝;
- 容量可以按需增長(類似于 JDK 的 StringBuilder);
- 在讀和寫這兩種模式之間切換不需要調(diào)用 ByteBuffer 的 flip()方法;
- 讀和寫使用了不同的索引;
- 支持方法的鏈式調(diào)用;
- 支持引用計數(shù);
- 支持池化。
對比ByteBuffer的缺點:
ByteBuffer
長度固定,一旦分配完成,它的容量不能動態(tài)擴展和收縮,當需要編碼的POJO對象大于ByteBuffer
的容量時,會發(fā)生索引越界異常;ByteBuffer
只有一個標識位置的指針position
,讀寫的時候需要手工調(diào)用flip()
和rewind()
等,使用者必須小心謹慎地處理這些API,否則很容易導(dǎo)致程序處理失??;ByteBuffer
的API功能有限,一些高級和實用的特性它不支持,需要使用者自己編程實現(xiàn)。
2、ByteBuf 類——Netty 的數(shù)據(jù)容器
所有的網(wǎng)絡(luò)通信都涉及字節(jié)序列的移動,所以高效易用的數(shù)據(jù)結(jié)構(gòu)明顯是必不可少的。所以理解Netty 的 ByteBuf 是如何滿足這些需求的很重要。
2.1 工作原理
ByteBuf
工作機制:ByteBuf
維護了兩個不同的索引,一個用于讀取,一個用于寫入。readerIndex
和writerIndex
的初始值都是0,當從ByteBuf
中讀取數(shù)據(jù)時,它的readerIndex
將會被遞增(它不會超過writerIndex
),當向ByteBuf
寫入數(shù)據(jù)時,它的writerIndex
會遞增。
ByteBuf
的幾個特點:
- 名稱以
readXXX
或者writeXXX
開頭的ByteBuf
方法,會推進對應(yīng)的索引,而以setXXX
或getXXX
開頭的操作不會。 - 在讀取之后,
0~readerIndex
的就被視為discard
的,調(diào)用discardReadBytes
方法,可以釋放這部分空間,它的作用類似ByteBuffer
的compact()
方法。 readerIndex
和writerIndex
之間的數(shù)據(jù)是可讀取的,等價于ByteBuffer
的position
和limit
之間的數(shù)據(jù)。writerIndex
和capacity
之間的空間是可寫的,等價于ByteBuffer
的limit
和capacity
之間的可用空間。
2.2 ByteBuf的三種類型
堆緩沖區(qū)
最常用的 ByteBuf 模式是將數(shù)據(jù)存儲在 JVM 的堆空間中。這種模式被稱為支撐數(shù)組(backing array),它能在沒有使用池化的情況下提供快速的分配和釋放
優(yōu)點
:由于數(shù)據(jù)存儲在JVM的堆中可以快速創(chuàng)建和快速釋放,并且提供了數(shù)組的直接快速訪問的方法。
缺點
:每次讀寫數(shù)據(jù)都要先將數(shù)據(jù)拷貝到直接緩沖區(qū)(相關(guān)閱讀:Java NIO 直接緩沖區(qū)和非直接緩沖區(qū)對比)再進行傳遞。
示例:
// 創(chuàng)建一個堆緩沖區(qū) ByteBuf buffer = Unpooled.buffer(10); String s = "waylau"; buffer.writeBytes(s.getBytes()); // 檢查是否是支撐數(shù)組 if (buffer.hasArray()) { // 獲取支撐數(shù)組的引用 byte[] array = buffer.array(); // 計算第一個字節(jié)的偏移量 int offset = buffer.readerIndex() + buffer.arrayOffset(); // 可讀字節(jié)數(shù) int length = buffer.readableBytes(); // 使用數(shù)組、偏移量和長度作為參數(shù)調(diào)用自定義的使用方法 printBuffer(array, offset, length); } /** * 打印出Buffer的信息 * * @param buffer */ private static void printBuffer(byte[] array, int offset, int len) { System.out.println("array:" + array); System.out.println("array->String:" + new String(array)); System.out.println("offset:" + offset); System.out.println("len:" + len); } /** 輸出結(jié)果: array:[B@5b37e0d2 array->String:waylau offset:0 len:6 */
直接緩沖區(qū)
Direct Buffer在堆之外直接分配內(nèi)存,直接緩沖區(qū)不會占用堆的容量。
優(yōu)點
:在使用Socket傳遞數(shù)據(jù)時性能很好,由于數(shù)據(jù)直接在內(nèi)存中,不存在從JVM拷貝數(shù)據(jù)到直接緩沖區(qū)的過程,性能好。
缺點
:因為Direct Buffer是直接在內(nèi)存中,所以分配內(nèi)存空間和釋放內(nèi)存比堆緩沖區(qū)更復(fù)雜和慢。
示例:
// 創(chuàng)建一個直接緩沖區(qū) ByteBuf buffer = Unpooled.directBuffer(10); String s = "waylau"; buffer.writeBytes(s.getBytes()); // 檢查是否是支撐數(shù)組. // 不是支撐數(shù)組,則為直接緩沖區(qū) if (!buffer.hasArray()) { // 計算第一個字節(jié)的偏移量 int offset = buffer.readerIndex(); // 可讀字節(jié)數(shù) int length = buffer.readableBytes(); // 獲取字節(jié)內(nèi)容 byte[] array = new byte[length]; buffer.getBytes(offset, array); // 使用數(shù)組、偏移量和長度作為參數(shù)調(diào)用自定義的使用方法 printBuffer(array, offset, length); } /** * 打印出Buffer的信息 * * @param buffer */ private static void printBuffer(byte[] array, int offset, int len) { System.out.println("array:" + array); System.out.println("array->String:" + new String(array)); System.out.println("offset:" + offset); System.out.println("len:" + len); } /** 輸出結(jié)果: array:[B@6d5380c2 array->String:waylau offset:0 len:6 */
復(fù)合緩沖區(qū)
復(fù)合緩沖區(qū)是 Netty 特有的緩沖區(qū)。本質(zhì)上類似于提供一個或多個 ByteBuf
的組合視圖,可以根據(jù)需要添加和刪除不同類型的 ByteBuf
。
優(yōu)點
:提供了一種訪問方式讓使用者自由地組合多個ByteBuf
,避免了復(fù)制和分配新的緩沖區(qū)。
缺點
:不支持訪問其支撐數(shù)組。因此如果要訪問,需要先將內(nèi)容復(fù)制到堆內(nèi)存中,再進行訪問。
示例:
// 創(chuàng)建一個堆緩沖區(qū) ByteBuf heapBuf = Unpooled.buffer(3); String way = "way"; heapBuf.writeBytes(way.getBytes()); // 創(chuàng)建一個直接緩沖區(qū) ByteBuf directBuf = Unpooled.directBuffer(3); String lau = "lau"; directBuf.writeBytes(lau.getBytes()); // 創(chuàng)建一個復(fù)合緩沖區(qū) CompositeByteBuf compositeBuffer = Unpooled.compositeBuffer(10); compositeBuffer.addComponents(heapBuf, directBuf); // 將緩沖區(qū)添加到符合緩沖區(qū) // 檢查是否是支撐數(shù)組. // 不是支撐數(shù)組,則為復(fù)合緩沖區(qū) if (!compositeBuffer.hasArray()) { for (ByteBuf buffer : compositeBuffer) { // 計算第一個字節(jié)的偏移量 int offset = buffer.readerIndex(); // 可讀字節(jié)數(shù) int length = buffer.readableBytes(); // 獲取字節(jié)內(nèi)容 byte[] array = new byte[length]; buffer.getBytes(offset, array); // 使用數(shù)組、偏移量和長度作為參數(shù)調(diào)用自定義的使用方法 printBuffer(array, offset, length); } } /** * 打印出Buffer的信息 * * @param buffer */ private static void printBuffer(byte[] array, int offset, int len) { System.out.println("array:" + array); System.out.println("array->String:" + new String(array)); System.out.println("offset:" + offset); System.out.println("len:" + len); } /** 輸出結(jié)果: array:[B@4d76f3f8 array->String:way offset:0 len:3 array:[B@2d8e6db6 array->String:lau offset:0 len:3 */
3、字節(jié)級操作
ByteBuf 提供了許多超出基本讀、寫操作的方法用于修改它的數(shù)據(jù)。
3.1 隨機訪問索引和順序訪問索引
如同在普通的 Java 字節(jié)數(shù)組中一樣,ByteBuf 的索引是從零開始的:第一個字節(jié)的索引是 0,最后一個字節(jié)的索引總是 數(shù)組容量 - 1。在ByteBuf的實現(xiàn)類中都有一個方法可以快速獲得容量值,那就是capacity()。有了這個capcity()方法,我們就能很簡單的實現(xiàn)隨機訪問索引:
for (int i = 0;i<buf.capacity();i++){ char b = (char)buf.getByte(i);//通過 getBytes 系列接口來對ByteBuf進行隨機訪問。 System.out.println(b); }
Tips: 用getBytes隨機訪問不會改變readerIndex
通過 readerIndex() 和 writerIndex()
獲取讀Index和寫Index。
3.2 可丟棄字節(jié)
在上圖中標記為可丟棄字節(jié)的分段包含了已經(jīng)被讀過的字節(jié)。通過調(diào)用 discardReadBytes
()方法,可以丟棄它們并回收空間。這個分段的初始大小為 0,存儲在 readerIndex
中, 會隨著 read 操作的執(zhí)行而增加(get操作不會移動 readerIndex
)。
下圖展示了在上圖的緩沖區(qū)上調(diào)用discardReadBytes
()方法后的結(jié)果,需要注意的是丟棄并不是字節(jié)把已經(jīng)讀的字段的字節(jié)不要了,而是把尚未讀的字節(jié)數(shù)移到最開始。(這樣做對可寫分段的內(nèi)容并沒有任何的保證,因為只是移動了可以讀取的字節(jié)以及 writerIndex,而沒有對所有可寫入的字節(jié)進行擦除寫)
3.3 可讀字節(jié)
ByteBuf
的可讀字節(jié)分段存儲了實際數(shù)據(jù)。新分配的、包裝的或者復(fù)制的緩沖區(qū)的默認的 readerIndex
值為 0。任何名稱以 read
或者 skip
開頭的操作都將檢索或者跳過位于當前 readerIndex
之前的數(shù)據(jù),并且在readerIndex
的基礎(chǔ)上增加已讀字節(jié)數(shù)。 如果被調(diào)用的方法需要一個 ByteBuf
參數(shù)作為寫入的目標,并且沒有指定目標索引參數(shù), 那么該目標緩沖區(qū)的 writerIndex
也將被增加。
3.4 可寫字節(jié)
可寫字節(jié)分段是指一個擁有未定義內(nèi)容的、寫入就緒的內(nèi)存區(qū)域。新分配的緩沖區(qū)的 writerIndex 的默認值為 0。任何名稱以 write
開頭的操作都將從當前的 writerIndex
處 開始寫數(shù)據(jù),并且在writerIndex
的基礎(chǔ)上增加已寫字節(jié)數(shù)。如果寫操作的目標也是 ByteBuf
,并且沒有指定 源索引的值,則源緩沖區(qū)的 readerIndex
也同樣會被增加相同的大小。
3.5 索引管理
JDK 的 InputStream
定義了 mark(int readlimit)
和 reset()
方法,這些方法分別 被用來將流中的當前位置標記為指定的值,以及將流重置到該位置。
同樣,可以通過調(diào)用 markReaderIndex()
、markWriterIndex()
、resetWriterIndex()
和 resetReaderIndex()
來標記和重置 ByteBuf
的 readerIndex
和 writerIndex
。這些和 InputStream
上的調(diào)用類似,只是沒有 readlimit
參數(shù)來指定標記什么時候失效
也可以通過調(diào)用 readerIndex(int)
或者 writerIndex(int)
來將索引移動到指定位置。試 圖將任何一個索引設(shè)置到一個無效的位置都將導(dǎo)致一個 IndexOutOfBoundsException
。
可以通過調(diào)用 clear()
方法來將 readerIndex
和 writerIndex
都設(shè)置為 0。注意,這 并不會清除內(nèi)存中的內(nèi)容。調(diào)用后的存儲結(jié)構(gòu)如下圖所示:
調(diào)用 clear()比調(diào)用 discardReadBytes()輕量得多,因為它將只是重置索引而不會復(fù) 制任何的內(nèi)存
3.6 派生緩沖區(qū)
派生緩沖區(qū)為 ByteBuf
提供了以專門的方式來呈現(xiàn)其內(nèi)容的視圖。這類視圖是通過以下方法被創(chuàng)建的:
- duplicate();
- slice();
- slice(int, int);
- Unpooled.unmodifiableBuffer(…);
- order(ByteOrder);
- readSlice(int)。
每個這些方法都將返回一個新的 ByteBuf 實例,它具有自己的讀索引、寫索引和標記 索引。其內(nèi)部存儲和 JDK 的 ByteBuffer 一樣也是共享的。這使得派生緩沖區(qū)的創(chuàng)建成本是很低廉的,但是這也意味著,如果你修改了它的內(nèi)容,也同時修改了其對應(yīng)的源實例,所以要小心。
ByteBuf 復(fù)制 如果需要一個現(xiàn)有緩沖區(qū)的真實副本,請使用 copy()或者 copy(int, int)方 法。不同于派生緩沖區(qū),由這個調(diào)用所返回的 ByteBuf 擁有獨立的數(shù)據(jù)副本。
3.7 讀/寫操作
有兩種類別的讀/寫操作:
- get()和 set()操作,從給定的索引開始,并且保持索引不變;
- read()和 write()操作,從給定的索引開始,并且會根據(jù)已經(jīng)訪問過的字節(jié)數(shù)對索引進行調(diào)整。
常用的 get()方法如下圖:
常用的 set()方法如下圖:
常用的 read()方法如下圖:
常用的 write()方法如下圖:
4、ByteBufHolder 接口
4.1 按需分配:ByteBufAllocator 接口
Netty 中內(nèi)存分配有一個最頂層的抽象就是ByteBufAllocator,負責分配所有ByteBuf 類型的內(nèi)存。他的一些操作如下:
可以通過 Channel
(每個都可以有一個不同的 ByteBufAllocator
實例)或者綁定到 ChannelHandler
的 ChannelHandlerContext
獲取一個到 ByteBufAllocator
的引用,如下圖所示:
Netty提供了兩種ByteBufAllocator
的實現(xiàn):PooledByteBufAllocator
和UnpooledByteBufAllocator
。前者池化了ByteBuf的實例以提高性能并最大限度地減少內(nèi)存碎片,這是通過一種 叫做jemalloc
的方法來分配內(nèi)存的(閱讀資料:jemalloc剖析)。后者的實現(xiàn)不池化ByteBuf
實例,并且在每次它被調(diào)用時都會返回一個新的實例。
4.2 Unpooled 緩沖區(qū)
可能某些情況下,你未能獲取一個到 ByteBufAllocator 的引用。對于這種情況,Netty 提 供了一個簡單的稱為 Unpooled 的工具類,它提供了靜態(tài)的輔助方法來創(chuàng)建未池化的 ByteBuf 實例。他的一些操作如下:
4.3 ByteBufUtil 類
ByteBufUtil
提供了用于操作 ByteBuf
的靜態(tài)的輔助方法。這個 API 是通用的,并且和池化無關(guān)。
5、引用計數(shù)
引用計數(shù)是一種通過在某個對象所持有的資源不再被其他對象引用時釋放該對象所持有的資源來優(yōu)化內(nèi)存使用和性能的技術(shù)。Netty
在第 4 版中為 ByteBuf
和 ByteBufHolder
引入了 引用計數(shù)技術(shù),它們都實現(xiàn)了 interface ReferenceCounted
。
5.1 基本原理
一個新創(chuàng)建的引用計數(shù)對象的初始引用計數(shù)是1。
ByteBuf buf = ctx.alloc().directbuffer(); assert buf.refCnt() == 1;
當你釋放掉引用計數(shù)對象,它的引用次數(shù)減1.如果一個對象的引用計數(shù)到達0,該對象就會被釋放或者歸還到創(chuàng)建它的對象池。
assert buf.refCnt() == 1; // release() returns true only if the reference count becomes 0. boolean destroyed = buf.release(); assert destroyed; assert buf.refCnt() == 0;
訪問引用計數(shù)為0的引用計數(shù)對象會觸發(fā)一次IllegalReferenceCountException。
assert buf.refCnt() == 0; try { buf.writeLong(0xdeadbeef); throw new Error("should not reach here"); } catch (IllegalReferenceCountExeception e) { // Expected }
只要引用計數(shù)對象未被銷毀,就可以通過調(diào)用retain()方法來增加引用次數(shù)。
ByteBuf buf = ctx.alloc().directBuffer(); assert buf.refCnt() == 1; buf.retain(); assert buf.refCnt() == 2; boolean destroyed = buf.release(); assert !destroyed; assert buf.refCnt() == 1;
5.2 誰來銷毀
一般的原則是,最后訪問引用計數(shù)對象的部分負責對象的銷毀。更具體地來說:
- 如果一個[發(fā)送]組件要傳遞一個引用計數(shù)對象到另一個[接收]組件,發(fā)送組件通常不需要 負責去銷毀對象,而是將這個銷毀的任務(wù)推延到接收組件
- 如果一個組件消費了一個引用計數(shù)對象,并且不知道誰會再訪問它(例如,不會再將引用 發(fā)送到另一個組件),那么,這個組件負責銷毀工作。
5.3 內(nèi)存泄漏問題
引用計數(shù)的缺點是,引用計數(shù)對象容易發(fā)生泄露。因為JVM并不知道Netty的引用計數(shù)實現(xiàn),當引用計數(shù)對象不 可達時,JVM就會將它們GC掉,即時此時它們的引用計數(shù)并不為0。一旦對象被GC就不能再訪問,也就不能歸還到緩沖池,所以會導(dǎo)致內(nèi)存泄露。 慶幸的是,盡管發(fā)現(xiàn)內(nèi)存泄露很難,但是Netty會對分配的緩沖區(qū)的1%進行采樣,來檢查你的應(yīng)用中是否存在內(nèi)存泄露。
內(nèi)存泄露檢查等級
總共有4個內(nèi)存泄露檢查等級:
- DISABLED – 完全禁用檢查。不推薦。
- SIMPLE – 檢查1%的緩沖區(qū)是否存在內(nèi)存泄露。默認。
- ADVANCED – 檢查1%的緩沖區(qū),并提示發(fā)生內(nèi)存泄露的位置
- PARANOID – 與ADVANCED等級一樣,不同的是會檢查所有的緩沖區(qū)。對于自動化測試很有用,你可以讓構(gòu)建測試失敗 如果構(gòu)建輸出中包含’LEAK’ 用JVM選項
-Dio.netty.leakDetectionLevel
來指定內(nèi)存泄露檢查等級
避免泄露最佳實踐
- 指定SIMPLE和PARANOI等級,運行單元測試和集成測試
- 在將你的應(yīng)用部署到整個集群前,盡可能地用足夠長的時間,使用SIMPLE級別去調(diào)試你的程序,來看是否存在內(nèi)存泄露
- 如果存在內(nèi)存泄露,使用ADVANCED級別去調(diào)試程序,去獲取內(nèi)存泄漏的位置信息
- 不要將存在內(nèi)存泄漏的應(yīng)用部署到整個集群
以上就是Netty核心功能之數(shù)據(jù)容器ByteBuf詳解的詳細內(nèi)容,更多關(guān)于Netty 數(shù)據(jù)容器ByteBuf的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot2.0+elasticsearch5.5+rabbitmq搭建搜索服務(wù)的坑
這篇文章主要介紹了springboot2.0+elasticsearch5.5+rabbitmq搭建搜索服務(wù)的坑,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06java程序員自己的圖片轉(zhuǎn)文字OCR識圖工具分享
這篇文章主要介紹了java程序員自己的圖片轉(zhuǎn)文字OCR識圖工具,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11Springboot mybatisplus如何解決分頁組件IPage失效問題
這篇文章主要介紹了Springboot mybatisplus如何解決分頁組件IPage失效問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08Java使用組合模式實現(xiàn)表示公司組織結(jié)構(gòu)功能示例
這篇文章主要介紹了Java使用組合模式實現(xiàn)表示公司組織結(jié)構(gòu)功能,簡單描述了組合模式的概念、功能并結(jié)合實例形式分析了Java使用組合模式實現(xiàn)公司組織結(jié)構(gòu)表示功能具體操作步驟與相關(guān)注意事項,需要的朋友可以參考下2018-05-05spring-security關(guān)閉登錄框的實現(xiàn)示例
這篇文章主要介紹了spring-security關(guān)閉登錄框的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-05-05