Netty高效能ByteBuf原始碼解析
本文地址: juejin.im/post/5db8ea…
Netty 高效能的原因除了前面提到的 NIO 的 Reactor 執行緒模型,零拷貝也是其高效能的一個重要原因.(其實原始碼解讀放到了最後)
零拷貝
- 省去了資料從使用者程式到核心的拷貝(jvm 堆內的資料 os 是不能直接使用的,要讓os可以使用的話,需要將堆內的資料拷貝一份到堆外)
- CompositeByteBuf 複合多個 ByteBuf,netty使用的是邏輯上的關聯,對外提供訪問的統一介面,而不是重新申請記憶體再將資料寫入新的 ByteBuf
Netty 的 ByteBuf 型別
-
Pooled(池化)、Unpooled(非池化)
-
Direct(直接緩衝區/堆外)、Heap(jvm堆內)
-
unsafe(unsafe 呼叫的本地方法)、safe(一般也不會這麼說,這是相對於 unsafe,指的jvm 堆內的操作)
Netty 預設都會優先使用 unsafe 的實現
池化/非池化(Pooled/Unpooled):
Netty 先申請一塊連續的空間作為 ByteBuf 池,需要用到的時候直接去池裡面取,用完之後返還給 ByteBuf 池,而不需要每次要用 ByteBuf 的時候都去申請. 堆外物件的建立比堆內的耗時.
總結: 池化的作用就是加快程式獲取到操作的物件
堆外/堆內(direct/heap):
堆內指的在 JVM 中的資料,申請、操作都是在jvm裡.
堆外的直接緩衝區指的是申請記憶體的時候用的 native 方法申請的 非jvm堆 的記憶體,這一部分記憶體 OS 是可以直接使用的,不像堆內的記憶體OS要使用的話還需要複製一次到直接緩衝區.申請的是堆外的記憶體,這時候 java 中的物件(DirectByteBuf)只是一些reader/writer Index(memory(記憶體地址),offset(偏移量) 等)的處理,寫資料/讀資料都是通過 native 對堆外的資料在進行操作.
總結:用堆外記憶體是為了防止物件的拷貝,提升效率
unsafe
unsafe 這個東西是 sun.misc 中提供的一個類,用這個類可以直接通過 native 方法操作記憶體,當然也會有效率提升,上面說的申請和操作堆外記憶體就是用這個叫做 unsafe 的東西來完成的. 但是用這個 unsafe 必須對記憶體操作非常熟悉,不然非常容易出錯,所以官方為什麼把它叫做 unsafe 也是有道理的.
總結: 直接操作記憶體,效率提升,使用容易出錯
Pool用到的一些類和概念
PoolArena、PoolChunk、PoolThreadLocalCache、PoolSubpage、Recycler
- PoolArena: Arena舞臺的意思,顧名思義是池中的操作需要這個類提供環境
- PoolChunk: Netty 中申請的記憶體塊,儲存 chunkSize,自身 offset,剩餘空間大小 freeSize 等資訊,按照官方說明: 為了在塊(chunk)中找到至少能滿足請求的大小,構造了一顆完全二叉樹,像堆(這是一個最大堆,chunk 中的節點將組成一顆完全二叉樹)
- PoolThreadLocalCache: 執行緒本地變數,儲存 PoolArena -> chunk ( -> page-> subPage)
- PoolSubPage: 位於最底層的 chunk 上的 Page
- Recycle: 顧名思義回收站,這是一個抽象類,主要作用從 ThreadLocal 中獲取到回收站裡的 ByteBuf
簡單說明: PoolThreadLocalCache 和 Recycle 都使用了 ThreadLocal 變數,減少多執行緒的爭搶,提升操作效率.
幾個重要的屬性值.
maxOrder 預設11 : 完全二叉樹的深度(根節點是第0層,所以客觀來說的一共有 maxOrder+1 層)
pageSize 預設8192 (8k) : 上面完全二叉樹的最底層的葉子結點 page 的預設大小
pageShifts 預設13: 這個是 pageSize 的對數,2^pageShifts = pageSize,pageSize 預設為 8192,所以這個預設值為 13
chunkSize 預設 16m(pageSize * maxOrder): 這個是每個 chunk 的大小,就是下面 chunk圖 的每一層的大小
一個page裡面的最小劃分單位為16byte,16這個數字很重要,後續有幾個關鍵計算的地方使用到
ByteBuf 的大小型別:
- size < 512,tiny
- 512 < size < 8192,small
- 8192 < size < 16m,normal
- 16m < size,huge
chunk 的結構
每一層的總和都是16m,一直細分到最底層,每個 page 為 8192(8k),所以最底層有2k個節點,這裡當然沒有全部畫出來,subPage 都在page上進行操作.
堆外/堆內ByteBuffer記憶體申請
一個簡單測試
做一個簡單測試,測試堆外記憶體的申請和堆內記憶體申請的耗時: static void nioAllocTest(){
int num = 10;
int cnt = 100;
int size = 256;
ByteBuffer buf;
long start1,end1,start2,end2;
long sum1,sum2;
for(int i = 0;i<num;i++){
sum1=sum2=0;
int j;
for(j = 0;j<cnt;j++) {
start1 = System.nanoTime();
buf = ByteBuffer.allocateDirect(size);
end1 = System.nanoTime();
sum1+=(end1-start1);
// System.out.println("direct 申請時間: "+(end1-start1));
start2 = System.nanoTime();
buf = ByteBuffer.allocate(size);
end2 = System.nanoTime();
// System.out.println("heap 申請時間: "+(end2-start2));
// System.out.println("-----");
sum2+=(end2-start2);
}
System.out.println(String.format("第 %s 輪申請 %s 次 %s 位元組平均耗時 [direct: %s,heap: %s].",i,j,size,sum1/cnt,sum2/cnt));
}
}
複製程式碼
輸出結果為:
第 0 輪申請 100 次 256 位元組平均耗時 [direct: 4864,heap: 1616].
第 1 輪申請 100 次 256 位元組平均耗時 [direct: 5763,heap: 1641].
第 2 輪申請 100 次 256 位元組平均耗時 [direct: 4771,heap: 1672].
第 3 輪申請 100 次 256 位元組平均耗時 [direct: 4961,heap: 883].
第 4 輪申請 100 次 256 位元組平均耗時 [direct: 3556,heap: 870].
第 5 輪申請 100 次 256 位元組平均耗時 [direct: 5159,heap: 726].
第 6 輪申請 100 次 256 位元組平均耗時 [direct: 3739,heap: 843].
第 7 輪申請 100 次 256 位元組平均耗時 [direct: 3910,heap: 221].
第 8 輪申請 100 次 256 位元組平均耗時 [direct: 2191,heap: 590].
第 9 輪申請 100 次 256 位元組平均耗時 [direct: 1624,heap: 615].
可以看到 direct 堆外記憶體的申請耗時明顯多於 jvm堆的申請耗時,這裡的耗時是幾倍(測試次數的不多可能不太準確,感興趣的同學可以測試更大/更小的size,可能會發現一些“有趣”的事).
池化/非池化
一個簡單測試
做一個簡單測試,測試池化的效果 static void nettyPooledTest(){
try {
int num = 10;
int cnt = 100;
int size = 8192;
ByteBuf direct1,direct2,heap1,heap2;
long start1,end2,start3,end3,start4,end4;
long sum1,sum2,sum3,sum4;
for (int i = 0; i<num; i++) {
sum1 = sum2 = sum3 = sum4 = 0;
int j;
for (j = 0; j<cnt; j++) {
start1 = System.nanoTime();
direct1 = PooledByteBufAllocator.DEFAULT.directBuffer(size);
end1 = System.nanoTime();
sum1 += (end1-start1);
start2 = System.nanoTime();
direct2 = UnpooledByteBufAllocator.DEFAULT.directBuffer(size);
end2 = System.nanoTime();
sum2 += (end2-start2);
start3 = System.nanoTime();
heap1 = PooledByteBufAllocator.DEFAULT.heapBuffer(size);
end3 = System.nanoTime();
sum3 += (end3-start3);
start4 = System.nanoTime();
heap2 = UnpooledByteBufAllocator.DEFAULT.heapBuffer(size);
end4 = System.nanoTime();
sum4 += (end4-start4);
direct1.release();
direct2.release();
heap1.release();
heap2.release();
}
System.out.println(String.format("Netty 第 %s 輪申請 %s 次 [%s] 位元組平均耗時 [direct.pooled: [%s],direct.unpooled: [%s],heap.pooled: [%s],heap.unpooled: [%s]].",sum2/cnt,sum3/cnt,sum4/cnt));
}
}catch(Exception e){
e.printStackTrace();
}finally {
}
}
複製程式碼
最終輸出的結果:
Netty 第 0 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [1784931],direct.unpooled: [105310],heap.pooled: [202306],heap.unpooled: [23317]].
Netty 第 1 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [12849],direct.unpooled: [15457],heap.pooled: [12671],heap.unpooled: [12693]].
Netty 第 2 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [13589],direct.unpooled: [14459],heap.pooled: [18783],heap.unpooled: [13803]].
Netty 第 3 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [10185],direct.unpooled: [11644],heap.pooled: [9809],heap.unpooled: [12770]].
Netty 第 4 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [15980],direct.unpooled: [53980],heap.pooled: [5641],heap.unpooled: [12467]].
Netty 第 5 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [4903],direct.unpooled: [34215],heap.pooled: [6659],heap.unpooled: [12311]].
Netty 第 6 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [2445],direct.unpooled: [7197],heap.pooled: [2849],heap.unpooled: [11010]].
Netty 第 7 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [2578],direct.unpooled: [4750],heap.pooled: [3904],heap.unpooled: [255689]].
Netty 第 8 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [1855],direct.unpooled: [3492],heap.pooled: [37822],heap.unpooled: [3983]].
Netty 第 9 輪申請 100 次 [8192] 位元組平均耗時 [direct.pooled: [1932],direct.unpooled: [2961],heap.pooled: [1825],heap.unpooled: [6098]].
這裡看 DirectByteBuffer,頻繁的申請堆外記憶體的話,會降低服務端的效能,這時候池化的作用就顯現出來了.池化只需開始的時候申請一塊足夠大的記憶體,後續獲取物件只是從池裡取出,用完返還Pool,並非每次都單獨去申請,省去了後續使用從堆外申請空間的耗時.
ByteBuf 具體的實現
這裡就講個人感覺最重要的一個,也是 netty 預設使用的型別: PooledUnsafeDirectByteBuf
,我們也從它的申請 PooledByteBufAllocator.DEFAULT.directBuffer() 開始講起.
下面從PooledByteBufAllocator.DEFAULT.directBuffer()進入
// 到第一個要分析的方法
protected ByteBuf newDirectBuffer(int initialCapacity,int maxCapacity) {
// 從 threadlLocal 獲取一個執行緒本地快取池
PoolThreadCache cache = (PoolThreadCache)this.threadCache.get();
// 這個快取池包含 heap 和 direct 兩種,獲取直接快取池
PoolArena<ByteBuffer> directArena = cache.directArena;
Object buf;
if (directArena != null) {
buf = directArena.allocate(cache,initialCapacity,maxCapacity); // 這裡往下 -- 1
} else {
// 如果沒有堆外快取池,直接申請堆外的 ByteBuf,優先使用 unsafe
buf = PlatformDependent.hasUnsafe() ? UnsafeByteBufUtil.newUnsafeDirectByteBuf(this,maxCapacity) : new UnpooledDirectByteBuf(this,maxCapacity);
}
return toLeakAwareBuffer((ByteBuf)buf);
}
// 1 directArena.allocate(cache,maxCapacity);
PooledByteBuf<T> allocate(PoolThreadCache cache,int reqCapacity,int maxCapacity) {
// newByteBuf(maxCapacity); 有兩種實現,directArena 和 heapArena
// Pool 的為在 recycle 中重用一個 ByteBuf
PooledByteBuf<T> buf = newByteBuf(maxCapacity); // -- 2
allocate(cache,buf,reqCapacity); // -- 7
return buf;
}
// 2 newByteBuf(maxCapacity)
protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) {
// 優先使用 PooledUnsafeDirect
if (HAS_UNSAFE) {
// PooledUnsafeDirect
return PooledUnsafeDirectByteBuf.newInstance(maxCapacity); // -- 3
} else {
// PooledDirect
return PooledDirectByteBuf.newInstance(maxCapacity);
}
}
// 3 PooledUnsafeDirectByteBuf.newInstance
static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
// 從用於回收的 ThreadLocal 中獲取一個 ByteBuf
PooledUnsafeDirectByteBuf buf = RECYCLER.get(); // -- 4
// 重置 ByteBuf 的下標等
buf.reuse(maxCapacity); // -- 6
return buf;
}
// 4 Recycler.get()
public final T get() {
if (maxCapacityPerThread == 0) {
return newObject((Handle<T>) NOOP_HANDLE);
}
// 每個執行緒都有一個棧
Stack<T> stack = threadLocal.get();
// 彈出一個 handle
DefaultHandle<T> handle = stack.pop();
// 如果 stack 中沒有 handle 則新建一個
if (handle == null) {
handle = stack.newHandle();
// newObject 由呼叫者實現,不同的 ByteBuf 建立各自不同的 ByteBuf,需要由建立者實現
// handle.value is ByteBuf,從上面跟下來,所以這裡是 PooledUnsafeDirectByteBuf
handle.value = newObject(handle); // -- 5
}
// 返回一個 ByteBuf
return (T) handle.value;
}
// 5 Stack.pop(),從棧中取出一個 handle
DefaultHandle<T> pop() {
int size = this.size;
if (size == 0) {
if (!scavenge()) {
return null;
}
size = this.size;
}
size --;
// 取出棧最上面的 handle
DefaultHandle ret = elements[size];
elements[size] = null;
if (ret.lastRecycledId != ret.recycleId) {
throw new IllegalStateException("recycled multiple times");
}
// 重置這個 handle 的資訊
ret.recycleId = 0;
ret.lastRecycledId = 0;
this.size = size;
return ret;
}
// 6 重用 ByteBuf 之前需要重置一下之前的下標等
final void reuse(int maxCapacity) {
maxCapacity(maxCapacity);
setRefCnt(1);
setIndex0(0,0);
discardMarks();
}
複製程式碼
上面的1到6步,從 PoolThreadLocalCache 中獲取堆外的Arena,並且根據出需要的大小從 RECYCLE 中獲取一個執行緒本地的 ByteBuf 棧,從棧中彈出一個 ByteBuf 並且重置 ByteBuf 的讀寫下標等.
講到這裡,程式碼中第二步的就算跟蹤完了,接下來就是第七步開始了.
PooledByteBuf<T> allocate(PoolThreadCache cache,reqCapacity); // -- 7
return buf;
}
複製程式碼
上面講到從 RECYCLE 的 執行緒本地棧 中獲取到了一個 ByteBuf,並且重置了讀寫下標等. 接下來的才算是重點.我們繼續跟著程式碼走下去
// allocate(cache,reqCapacity); -- 7
// 這一段都很重要,程式碼複製比較多,normal(>8192) 和 huge(>16m) 的暫時不做分析
private void allocate(PoolThreadCache cache,PooledByteBuf<T> buf,final int reqCapacity) {
// 計算應該申請的大小
final int normCapacity = normalizeCapacity(reqCapacity); // -- 8
// 申請的大小是否小於一頁 (預設8192) 的大小
if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
int tableIdx;
PoolSubpage<T>[] table;
// reqCapacity < 512
boolean tiny = isTiny(normCapacity);
if (tiny) { // < 512 is tiny
// 申請 tiny 容量的空間
if (cache.allocateTiny(this,reqCapacity,normCapacity)) {
return;
}
// 計算屬於哪個子頁,tiny 以 16B 為單位
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;
} else {
//8192 > reqCapacity >= 512 is small
// small 以 1024為單位
if (cache.allocateSmall(this,normCapacity)) {
return;
}
tableIdx = smallIdx(normCapacity);
table = smallSubpagePools;
}
// head 指向自己在 table 中的位置的頭
final PoolSubpage<T> head = table[tableIdx];
/**
* Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
* {@link PoolChunk#free(long)} may modify the doubly linked list as well.
*/
synchronized (head) {
final PoolSubpage<T> s = head.next;
// 這裡判斷是否已經新增過 subPage
// 新增過的話,直接在該 subPage 上面進行操作,記錄標識位等
if (s != head) {
assert s.doNotDestroy && s.elemSize == normCapacity;
// 在 subPage 的 bitmap 中的下標
long handle = s.allocate();
assert handle >= 0;
// 用 已經初始化過的 bytebuf 初始化 subPage 中的資訊
s.chunk.initBufWithSubpage(buf,handle,reqCapacity);
// 計數
incTinySmallAllocation(tiny);
return;
}
}
// 第一次建立該型別大小的 ByteBuf,需要建立一個subPage
synchronized (this) {
allocateNormal(buf,normCapacity);
}
// 增加計數
incTinySmallAllocation(tiny);
return;
}
}
複製程式碼
計算應該申請的 ByteBuf 的大小
// 8 以下程式碼是在 normalizeCapacity(reqCapacity) 中
// 如果 reqCapacity >= 512,則使用 跟hashMap 相同的擴容演演算法
// reqCapacity < 512(tiny型別) 則將 reqCapacity 變成 16 的倍數
if (!isTiny(reqCapacity)) {
// 是不是很熟悉,有沒有印象 HashMap 的擴容,找一個不小於原數的2的指數次冪大小的數
int normalizedCapacity = reqCapacity;
normalizedCapacity --;
normalizedCapacity |= normalizedCapacity >>> 1;
normalizedCapacity |= normalizedCapacity >>> 2;
normalizedCapacity |= normalizedCapacity >>> 4;
normalizedCapacity |= normalizedCapacity >>> 8;
normalizedCapacity |= normalizedCapacity >>> 16;
normalizedCapacity ++;
//
if (normalizedCapacity < 0) {
normalizedCapacity >>>= 1;
}
assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0;
return normalizedCapacity;
}
// reqCapacity < 512
// 已經是16的倍數,不做操作
if ((reqCapacity & 15) == 0) {
return reqCapacity;
}
// 不是16的倍數,轉化為16的倍數
return (reqCapacity & ~15) + 16;
複製程式碼
因為 small 和 tiny
還是有比較多相似的,所以我們選 tiny
來講
// 申請 tiny 容量的空間
if (cache.allocateTiny(this,normCapacity)) {
return;
}
// 計算屬於哪個子頁,tiny 以 16b 為單位
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;
// head 指向自己在 table 中的位置的頭
final PoolSubpage<T> head = table[tableIdx];
複製程式碼
這裡看到 tinySubpagePools,看名字應該是儲存 tinySubPage 的地方,跟蹤一下可以看到,tinySubPage 在構造方法裡進行了初始化
tinySubpagePools = newSubpagePoolArray(numTinySubpagePools);
// 初始化 32 種型別的 subPage 的 head,這裡是記錄 head
for (int i = 0; i < tinySubpagePools.length; i ++) {
tinySubpagePools[i] = newSubpagePoolHead(pageSize);
}
// 512 / 16 = 32
static final int numTinySubpagePools = 512 >>> 4;
複製程式碼
numTinySubpagePools,這是一個靜態變數,512是small和tiny的邊界點,512 >>> 4 = 32,為什麼是無符號右移4位,還記得上面說的 subPage 分配的基本單位嗎,subPage 分配的基本單位就是 16byte,所以這裡是計算從16 到 512 以 16為單位 一共有多少種型別大小的 ByteBuf,tinySubpagePools->[16,32,48....512],上面的 tinyIdx(int normCapacity) 就是計算屬於哪種型別的ByteBuf 並獲取該型別 ByteBuf 在 tinySubpagePools 中的下標,後續就可以根據下標獲取到 pool 中對應下標的 head,建構函式中初始化了所有的 head,實際申請的話,不是用這個head來申請,而是會另外 new 一個 subPage,然後跟這個 head 形成雙向連結串列. 按照上面的程式碼順序,接下來就到了 poolSubPage(init or allocate)
subPage一些欄位的作用
final PoolChunk<T> chunk;
// 當前 subPage 所處的 Page 節點下標
private final int memoryMapIdx;
// 當前子頁的 head 在 該 chunk 中的偏移值,單位為 pageSize(default 8192)
private final int runOffset;
// default 8192
private final int pageSize;
// 預設 8 個 long 的位元組長度,long是64位,8*64 = 512,512 * 16(subPage最低按照16位元組分配) = 8192(one default page)
// 意思是將 一個page分為 512 個 16byte,每一個 16byte 用一位(bit)來標記是否使用,一個long有64bit,所以一共需要 512 / 64 = 8個long型別來作為標記位
private final long[] bitmap;
// 這個是指一個 Page 中最多可以儲存多少個 elemSize 大小 ByteBuf
// maxNumElems = pageSize / elemSize
private int maxNumElems;
// 已經容納多少個 elemSize 大小的 ByteBuf
private int numAvail;
// 這個是記錄真正能使用到的 bit 的length,因為你不可能每個 page 中的 elemSize 都是16,肯定是有其他大小的,在 PoolSubPage 的 init 方法中可以看到: bitmapLength = maxNumElems >>> 6;
private int bitmapLength;
// 所以初始化方法 init(),只初始化 bitmapLength 個 long 型別
/**
* for (int i = 0; i < bitmapLength; i ++) {
* bitmap[i] = 0;
* }
*/
複製程式碼
總結下來就是,一個 8192 大小的 page,先根據傳入的大小計算最多能容納多少個該大小的位元組陣列(堆外都是用位元組陣列) maxNumElems,再根據最大能容納的數量計算最多能用到多少個 long型別的數字作為標記位 bitmapLength,最後初始化bitmap,可見bitmap 是標記page中已經使用過的位置(以16byte為單位).
PoolSubPage 中還有一個很重要的方法: toHandle(); 這個方法的作用是將節點下標 memoryMapIdx 和 bitmapIdx 放到一起,用一個 long 型別來記錄.通過這個handle值,可以獲取到對應節點(根據 memoryMapIdx)和該節點(page)下對應的偏移位置(就是bitmapIdx * 16)
private long toHandle(int bitmapIdx) {
// 後續會用 (int)handle 將這個 handle 值變回為 memoryMapIdx,即所屬節點下標
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
複製程式碼
介紹完了 subPage 的欄位含義後,繼續跟蹤上面的程式碼:
這一段程式碼是在根據申請的大小獲取到對應下標的 head 節點後做的處理,s!=head 是判斷是否有申請過相同大小subPage,有的話直接 initBufWithSubpage在原有的 subPage 上進行操作,而不用呼叫後面的 allocateNormal(buf,normCapacity); 去allocate 一個新的 subPage
synchronized (head) {
final PoolSubpage<T> s = head.next;
// 這裡判斷是否已經新增過 subPage
// 新增過的話,記錄標識位等
if (s != head) {
assert s.doNotDestroy && s.elemSize == normCapacity;
// 在 subPage 的 bitmap 中的下標 && 節點下標
long handle = s.allocate();
assert handle >= 0;
// 用已經初始化過的 bytebuf 更新 subPage 中的資訊
s.chunk.initBufWithSubpage(buf,reqCapacity);
// 計數
incTinySmallAllocation(tiny);
return;
}
}
複製程式碼
initBufWithSubpage 方法跟蹤下去可以看到:
buf.init(
this,runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset,subpage.elemSize,arena.parent.threadCache());
複製程式碼
runOffset(memoryMapIdx): memoryMapIdx 為節點下標,runOffset 表示該節點在chunk中的偏移量,以 8192 為單位 節點偏移 (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize: 這個偏移量表示 bitmapIdx 下標在 subPage 中的偏移量
offset: 表示chunk自身的偏移.
這個3個offset 總和就是 bitmapIdx表示的下標在整個快取池中的具體偏移值
上面就是申請一個池化的ByteBuf的具體流程
本文屬作者個人理解,有什麼寫錯的地方望各位能指出.
最後
最後的最後,非常感謝你們能看到這裡!!你們的閱讀都是對作者的一次肯定!!!
覺得文章有幫助的看官順手點個贊再走唄(終於暴露了我就是來騙讚的(◒。◒)),你們的每個贊對作者來說都非常重要(異常真實),都是對作者寫作的一次肯定(double)!!!
這一篇的內容到這就結束了,期待下一篇 還能有幸遇見你!