Netty內存池值PoolSubpage詳解

在Netty內存池中,內存大小在8KB~16M的內存是由PoolChunk維護的,小於8KB的內存則是由PoolSubpage來維護的。而對於低於8KB的內存,Netty也是將其分成了兩種情況0~496byte和512byte~8KB。其中,0~496byte的內存是由一個名稱為tinySubpagePools的PoolSubpage的數組維護的,512byte~8KB的內存則是由名稱為smallSubpagePools的PoolSubpage數組來維護的。本文首先會對tinySubpagePools和smallSubpagePools的整體結構進行說明,然後會講解Netty是如何僅僅通過抽象出一種PoolSubpage的數據結構來實現對兩種不同的內存區間的管理的,最後本文會從PoolSubpage的源碼的角度來講解PoolSubpage的實現原理。

1. tinySubpagePools和smallSubpagePools整體結構

這裡我們直接查看這兩個PoolSubpage數組的結構:

Netty內存池值PoolSubpage詳解

Netty內存池值PoolSubpage詳解

  • tinySubpagePools和smallSubpagePools在結構上都是由一個數組來實現的,只是tinySubpagePools的數組長度為32,但是其真正使用的只有其下標在1~31內的節點。而smallSubpagePools的數組長度為4,其每個節點都會使用;
  • 在存儲數據內存的劃分上,圖中,我們可以看到,兩個數組的每個節點都是一個PoolSubpage的單向鏈表,而節點前面我們都使用了一個數字進行標註。這個數字的意思是,這個節點所對應的鏈表所能夠申請的內存最大值,這樣就可以達到將不同大小的內存申請進行了劃分,並且加鎖的時候可以減小鎖的粒度,從而減小競爭。這裡比如我們申請8byte的內存,那麼其就會到tinySubpagePools的下標為1的鏈表中進行申請,需要注意的是,如果該下標位置的鏈表是空的,那麼就會創建一個,但是一定會保證是在該下標處進行申請;
  • tinySubpagePools和smallSubpagePools的最大區別在於兩者對於內存的劃分。圖中我們可以看到,tinySubpagePools的每個節點所指代的內存相差16byte,而smallSubpagePools的內存則是2的指數次冪;
  • 在對內存的管理上,這裡每一個PoolSubpage也都是維護的一個內存池,它們的大小永遠都是8KB。這裡比如tinySubpagePools的第1號位的每一個PoolSubpage,其能夠申請的內存最大為16byte,由於每一個PoolSubpage的大小都為8KB,因而其鏈表中每個PoolSubpage都維護了8192 / 16 = 512個內存塊;由比如smallSubpagePools的第2號位的每一個PoolSubpage,其能夠申請的內存最大為2048byte,因而其鏈表中每一個PoolSubpage都維護了8192 / 2048 = 4個內存塊;
  • 在進行內存申請時,用戶會傳入一個其所希望的內存大小,但實際獲取的大小,Netty都會進行擴容,這裡我們以50byte內存的申請為例進行講解:
  • 首先Netty會判斷目標內存,這裡為50byte,是否小於8KB,只有小於8KB的內存才會交由tinySubpagePools和smallSubpagePools進行申請;進行了如此判斷之後,Netty還會判斷其是否小於512byte,從而判斷其是應該從tinySubpagePools還是從smallSubpagePools中進行申請,這裡50小於512,因而是從tinySubpagePools中進行申請;
  • 將目標內存進行擴容,因為已經知道其是從tinySubpagePools中進行申請,由於tinySubpagePools中的內存階梯是16的倍數,因而會將目標內存擴容為大於其值的第一個16的倍數,這裡也就是64。如果目標內存是在smallSubpagePools中,那麼就會將其擴容為大於該值的第一個2的指數次冪;
  • 根據擴容後的大小計算出其在數組中的位置,這裡就是64 / 16 = 4(在Netty源碼中是直接將目標內存向右移動4位,即64 >>> 4,這樣也能達到除以16的目的);
  • 在目標鏈表中找到第一個PoolSubpage,從其剩餘的內存中劃分目標內存塊,這裡需要注意的是,第一個PoolSubpage中是一定會存在可劃分的內存塊的,因為如果鏈表中某個PoolSubpage中沒有剩餘的可劃分內存塊時,其將會被從當前鏈表中移除。關於PoolSubpage內存塊的劃分,後面會進行詳細講解。

2. PoolSubpage實現原理講解

對於PoolSubpage的實現原理,其內部本質上是使用一個位圖索引來表徵某個內存塊是否已經被佔用了的。前面我們講到,每個PoolSubpage的總內存大小都是8192byte,這裡我們以tinySubpagePools的第1號位的大小為16字節的PoolSubpage為例進行講解(其實從這裡就可以看出,前面我們圖中數組前面的數字就是表示當前節點鏈表中PoolSubpage所劃分的內存塊的大小)。

由於每個內存塊大小為16字節,而總大小為8192字節,因而總會有8192 / 16 = 512個內存塊。為了對這些內存塊進行標記,那麼就需要一個長度為512的二進制位圖索引進行表徵。Netty並沒有使用jdk提供的BitMap這個類,而是使用了一個long型的數組。由於一個long佔用的字節數為64,因而總共需要512 / 64 = 8個long型數字來表示。這也就是PoolSubpage中的long[] bitmap屬性的作用。下圖表示了PoolSubpage使用位圖索引表示每個內存塊是否被使用的一個示意圖:

Netty內存池值PoolSubpage詳解

這裡需要說明的是,我們這裡是以每個內存塊的大小為16為例進行講解的,而16是PoolSubpage所能維護的最小內存塊,對於其他大小的內存塊,其個數是比512要小的,但是PoolSubpage始終會聲明一個長度為8的long型數組,並且聲明一個bitmapLength來記錄當前PoolSubpage中有幾個long是用於標誌內存塊使用情況的。

3. PoolSubpage源碼講解

對於PoolSubpage的實現原理,我們這裡首先對其各個屬性進行講解:

// 記錄當前PoolSubpage的8KB內存塊是從哪一個PoolChunk中申請到的
final PoolChunk chunk;
// 當前PoolSubpage申請的8KB內存在PoolChunk中memoryMap中的下標索引
private final int memoryMapIdx;
// 當前PoolSubpage佔用的8KB內存在PoolChunk中相對於葉節點的起始點的偏移量
private final int runOffset;
// 當前PoolSubpage的頁大小,默認為8KB
private final int pageSize;
// 存儲當前PoolSubpage中各個內存塊的使用情況
private final long[] bitmap;
PoolSubpage prev;\t// 指向前置節點的指針
PoolSubpage next;\t// 指向後置節點的指針
boolean doNotDestroy;\t// 表徵當前PoolSubpage是否已經被銷燬了
int elemSize;\t// 表徵每個內存塊的大小,比如我們這裡的就是16

private int maxNumElems;\t// 記錄內存塊的總個數
private int bitmapLength;\t// 記錄總共可使用的bitmap數組的元素的個數
// 記錄下一個可用的節點,初始為0,只要在該PoolSubpage中申請過一次內存,就會更新為-1,
// 然後一直不會發生變化
private int nextAvail;
// 剩餘可用的內存塊的個數
private int numAvail;

對於各個屬性的初始化,我們可以通過構造函數進行講解,如下是其構造函數源碼:

PoolSubpage(PoolSubpage head, PoolChunk chunk, int memoryMapIdx, int runOffset, 
int pageSize, int elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;\t// 初始化當前PoolSubpage總內存大小,默認為8KB
// 計算bitmap長度,這裡pageSize >>> 10其實就是將pageSize / 1024,得到的是8,
// 從這裡就可以看出,無論內存塊的大小是多少,這裡的bitmap長度永遠是8,因為pageSize始終是不變的
bitmap = new long[pageSize >>> 10];
// 對其餘的屬性進行初始化
init(head, elemSize);
}
void init(PoolSubpage head, int elemSize) {
doNotDestroy = true;

// elemSize記錄了當前內存塊的大小
this.elemSize = elemSize;
if (elemSize != 0) {
// 初始時,numAvail記錄了可使用的內存塊個數,其個數可以通過pageSize / elemSize計算,
// 我們這裡就是8192 / 16 = 512。maxNumElems指的是最大可使用的內存塊個數,
// 初始時其是與可用內存塊個數一致的。
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;\t// 初始時,nextAvail是0
// 這裡bitmapLength記錄了可以使用的bitmap的元素個數,這是因為,我們示例使用的內存塊大小是16,
// 因而其總共有512個內存塊,需要8個long才能記錄,但是對於一些大小更大的內存塊,比如smallSubpagePools
// 中內存塊為1024字節大小,那麼其只有8192 / 1024 = 8個內存塊,也就只需要一個long就可以表示,
// 此時bitmapLength就是8。
// 這裡的計算方式應該是bitmapLength = maxNumElems / 64,因為64是一個long的總字節數,
// 但是Netty將其進行了優化,也就是這裡的maxNumElems >>> 6,這是因為2的6次方正好為64
bitmapLength = maxNumElems >>> 6;
// 這裡(maxNumElems & 63) != 0就是判斷元素個數是否小於64,如果小於,則需要將bitmapLegth加一。
// 這是因為如果其小於64,前面一步的位移操作結果為0,但其還是需要一個long來記錄
if ((maxNumElems & 63) != 0) {
bitmapLength++;
}

// 對bitmap數組的值進行初始化
for (int i = 0; i < bitmapLength; i++) {
bitmap[i] = 0;
}
}

// 將當前PoolSubpage添加到PoolSubpage的鏈表中,也就是最開始圖中的鏈表
addToPool(head);
}

這裡對於PoolSubpage的初始化主要是對bitmap、numAvail、bitmapLength的初始化,下面我們看看其是如何通過這些屬性來從PoolSubpage中申請內存的:

// 對於allocate()方法,其沒有傳入任何參數是因為當前PoolSubpage所能申請的內存塊大小在構造方法中
// 已經通過elemSize指定了。
// 當前方法返回的是一個long型整數,這裡是將要申請的內存塊使用了一個long型變量進行表徵了。由於一個內存塊
// 是否使用是通過一個long型整數表示的,因而,如果想要表徵當前申請到的內存塊是這個long型整數中的哪一位,
// 只需要一個最大為63的整數即可(long最多為64位),這隻需要long型數的低6位就可以表示,由於我們使用的是一個
// long型數組,因而還需要記錄當前是在數組中第幾個元素,由於數組長度最多為8,因而對於返回值的7~10位則是記錄

// 了當前申請的內存塊是在bitmap數組的第幾個元素中。總結來說,返回值的long型數的高32位中的低6位
// 記錄了當前申請的是是bitmap中某個long的第幾個位置的內存塊,而高32位的7~10位則記錄了申請的是bitmap數組
// 中的第幾號元素。
// 這裡說返回值的高32位是因為其低32位記錄了當前8KB內存塊是在PoolChunk中具體的位置,關於這一塊的算法
// 讀者可以閱讀本人前面對PoolChunk進行講解的文章
long allocate() {
// 如果elemSize為0,則直接返回0
if (elemSize == 0) {
return toHandle(0);
}
// 如果當前PoolSubpage沒有可用的元素,或者已經被銷燬了,則返回-1
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
// 計算下一個可用的內存塊的位置
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;\t// 獲取該內存塊是bitmap數組中的第幾號元素
int r = bitmapIdx & 63;\t\t// 獲取該內存塊是bitmap數組中q號位元素的第多少位
bitmap[q] |= 1L << r;\t// 將bitmap數組中q號元素的目標內存塊位置標記為1,表示已經使用
// 如果當前PoolSubpage中可用的內存塊為0,則將其從鏈表中移除
if (--numAvail == 0) {
removeFromPool();
}

// 將得到的bitmapIdx放到返回值的高32位中
return toHandle(bitmapIdx);
}

這裡allocate()方法首先會計算下一個可用的內存塊的位置,然後將該位置標記為1,最後將得到的位置數據放到返回值的高32位中。這裡我們繼續看其是如何計算下一個可用的位置的,如下是getNextAvail()的源碼:

private int getNextAvail() {
int nextAvail = this.nextAvail;
// 如果是第一次嘗試獲取數據,則直接返回bitmap第0號位置的long的第0號元素,
// 這裡nextAvail初始時為0,在第一次申請之後就會變為-1,後面將不再發生變化,
// 通過該變量可以判斷是否是第一次嘗試申請內存
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}

// 如果不是第一次申請內存,則在bitmap中進行遍歷獲取
return findNextAvail();
}
private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
// 這裡的基本思路就是對bitmap數組進行遍歷,首先判斷其是否有未使用的內存是否全部被使用過
// 如果有未被使用的內存,那麼就在該元素中找可用的內存塊的位置

for (int i = 0; i < bitmapLength; i++) {
long bits = bitmap[i];
if (~bits != 0) {\t// 判斷當前long型元素中是否有可用內存塊
return findNextAvail0(i, bits);
}
}
return -1;
}
// 入參中i表示當前是bitmap數組中的第幾個元素,bits表示該元素的值
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
final int baseVal = i << 6;\t// 這裡baseVal就是將當前是第幾號元素放到返回值的第7~10號位置上
// 對bits的0~63號位置進行遍歷,判斷其是否為0,為0表示該位置是可用內存塊,從而將位置數據
// 和baseVal進行或操作,從而得到一個表徵目標內存塊位置的整型數據
for (int j = 0; j < 64; j++) {
if ((bits & 1) == 0) {\t// 判斷當前位置是否為0,如果為0,則表示是目標內存塊
int val = baseVal | j;\t// 將內存快的位置數據和其位置j進行或操作,從而得到返回值
if (val < maxNumElems) {
return val;
} else {
break;
}
}
bits >>>= 1;\t// 將bits不斷的向右移位,以找到第一個為0的位置
}
return -1;
}

上面的查找過程非常的簡單,其原理起始就是對bitmap數組進行遍歷,首先判斷當前元素是否有可用的內存塊,如果有,則在該long型元素中進行遍歷,找到第一個可用的內存塊,最後將表徵該內存塊位置的整型數據返回。這裡需要說明的是,上面判斷bitmap中某個元素是否有可用內存塊是使用的是~bits != 0來計算的,該算法的原理起始就是,如果一個long中所有的內存塊都被申請了,那麼這個long必然所有的位都為1,從整體上,這個long型數據的值就為-1,而將其取反~bits之後,值肯定就變為了0,因而這裡只需要判斷其取反之後是否等於0即可判斷當前long型元素中是否有可用的內存塊。

下面我們繼續看PoolSubpage是如何對內存進行釋放的,如下是free()方法的源碼:

boolean free(PoolSubpage head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}

// 獲取當前需要釋放的內存塊是在bitmap中的第幾號元素
int q = bitmapIdx >>> 6;
// 獲取當前釋放的內存塊是在q號元素的long型數的第幾位
int r = bitmapIdx & 63;
// 將目標位置標記為0,表示可使用狀態
bitmap[q] ^= 1L << r;
// 設置下一個可使用的數據
setNextAvail(bitmapIdx);
// numAvail如果等於0,表示之前已經被移除鏈表了,因而這裡釋放後需要將其添加到鏈表中
if (numAvail++ == 0) {
addToPool(head);
return true;
}
// 如果可用的數量小於最大數量,則表示其還是在鏈表中,因而直接返回true
if (numAvail != maxNumElems) {
return true;
} else {
// else分支表示當前PoolSubpage中沒有任何一個內存塊被佔用了
// 這裡如果當前PoolSubpage的前置節點和後置節點相等,這表示其都是默認的head節點,也就是
// 說當前鏈表中只有一個可用於內存申請的節點,也就是當前PoolSubpage,這裡就不會將其移除

if (prev == next) {
return true;
}
// 如果有多個節點,則將當前PoolSubpage移除
doNotDestroy = false;
removeFromPool();
return false;
}
}

可以看到,對於free()操作,主要是將目標位置標記為0,然後設置相關屬性,並且判斷是否需要將當前PoolSubpage添加到鏈表中或者從鏈表移除。

4. 小結

本文首先講解了PoolSubpage的實現原理,然後講解了其是如何控制內存的申請和釋放的,最後從源碼層面對其申請和釋放內存的行為進行了講解。


分享到:


相關文章: