上文已經瞭解到了Netty中的啓動器、反應器、通道、處理器、流水線,下面來了解一下Netty中較爲重要的ByteBuf緩衝區。
概念
優勢
- Pooling池化,減少了內存複製和GC,提升了效率。
- 讀寫分開存儲,索引也分開了,不需要切換讀寫模式。
- 方法可鏈式調用,引入了引用計數法,方便了池化與內存回收。
重要屬性
- readerIndex(讀指針):讀取的起始位置,每讀取一個字節,就加1,當它等於writerIndex時,說明已經讀完了。
- writerIndex(寫指針):寫入的起始位置,每寫入一個字節,就加1,當它等於capacity()時,說明當前容量滿了。此時可擴容,如果不能繼續擴容,則不能寫了。
- maxCapacity(最大容量):可以擴容的最大容量,當前容量等於這個值時,說明不能再擴容了。
引用計數
Netty採用“計數器”來追蹤ByteBuf的生命週期,主要是用於對池化的支持。(池化就是當ByteBuf的引用爲0時,就會放到對象緩存池中,當需要用緩衝區時,可以直接從這個池裏面取出來用,而不用重新創建一個了)。通過源碼可以發現ByteBuf實現了一個類ReferenceCounted。這個類就是用於引用計數的。
當創建完ByteBuf時,引用數爲1, 通過refCnt()方法可以獲取當前緩衝區的引用數,調用retain()方法可以使引用數加1,調用release()方法可以使引用數減1,當引用數爲0時,緩衝區就會被完全釋放。如果池化了就放到緩衝池中。如果沒池化就分兩種情況,如果是分配在堆內存上的,就通過JVM的垃圾回收機制把它回收,如果分配在堆外直接內存上,就通過本地方法來釋放堆外內存。在Handler處理器中,Netty會自動給流水線在最後加一個處理器用來調用release()去釋放緩衝區,如果要在中間中斷流水線,則需要自己調用release()釋放緩衝區。
Allocator分配器
Netty提供了ByteAllocator的兩種實現:PoolByteAllocator(池化)和UnpooledByteAllocator(未池化)。Netty默認使用的是PoolByteAllocator,默認使用的內存是堆外直接內存(寫入速度比堆內存更快,池化分配器配合堆外直接內存,可將堆外緩衝區複用(彌補了堆外分配和釋放空間的代價較高的缺點),從來大大提升了性能)。
淺層複製
淺層複製有兩個方法,切片淺層複製和整體淺層複製
- slice切片淺層複製 :切片只複製了原緩衝區的可讀部分,不會複製底層數組(引用同一個),也不會增加引用數。
- duplicate整體淺層複製 :這個是將整體都複製了,可讀可寫,但是引用還是一樣的(同slice)。
實踐
來寫個小例子,分析一下。這裏的Logger使用的是自己封裝的靜態日誌類。分析堆棧信息封裝一個SLF4J的靜態類
public class testBuffer {
public static void main(String[] args) {
// 默認使用池化緩衝區,分配的是堆外直接內存
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(9, 100);
// 使用堆內存來分配緩衝區內存
ByteBuf buf1 = ByteBufAllocator.DEFAULT.heapBuffer(9, 100);
// 寫入一個字節數組
buf.writeBytes(new byte[] { 1, 2, 3, 4 });
Logger.info("引用次數--[{}]", buf.refCnt());
// 依次讀出來
int i = 0;
while (buf.isReadable() && i < buf.readableBytes()) {
// 讀字節,不改變指針(讀完後,readIndex還是0)
Logger.info("取一個字節--[{}]", buf.getByte(i++));
}
Logger.info("buf是否使用的堆內存--[{}]", buf.hasArray());
Logger.info("buf1是否使用的堆內存--[{}]", buf1.hasArray());
int len = buf.readableBytes();
byte[] array = new byte[len];
// 把數據讀取到堆內存中
buf.getBytes(buf.readerIndex(), array);
Logger.info("buf讀出的數據--[{}]", array);
// 與原緩衝區buf的底層引用一樣
ByteBuf slice = buf.slice();
// 增加一次淺層複製的引用
slice.retain();
// 減少一次原緩衝區的引用
buf.release();
// 會發現兩個引用是同一個
Logger.info("引用次數--[{}]", buf.refCnt());
Logger.info("切片結果--{}", slice);
Logger.info("引用次數--[{}]", slice.refCnt());
}
}
運行結果:
21:02:59.693 [main] INFO byteBuf.test1 - 引用次數--[1]
21:02:59.695 [main] INFO byteBuf.test1 - 取一個字節--[1]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個字節--[2]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個字節--[3]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個字節--[4]
21:02:59.696 [main] INFO byteBuf.test1 - buf是否使用的堆內存--[false]
21:02:59.696 [main] INFO byteBuf.test1 - buf1是否使用的堆內存--[true]
21:02:59.696 [main] INFO byteBuf.test1 - buf讀出的數據--[[1, 2, 3, 4]]
21:02:59.698 [main] INFO byteBuf.test1 - 引用次數--[1]
21:02:59.698 [main] INFO byteBuf.test1 - 切片結果--UnpooledSlicedByteBuf(ridx: 0, widx: 4, cap: 4/4, unwrapped: PooledUnsafeDirectByteBuf(ridx: 0, widx: 4, cap: 9/100))
21:02:59.698 [main] INFO byteBuf.test1 - 引用次數--[1]