初始Netty原理 (二)

上文已經瞭解到了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]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章