Netty源碼分析:PoolChunk
Chunk主要用來組織和管理多個Page的內存分配和釋放。在Netty中,Chunk中的Page被構建成一顆二叉樹。本博文將從源碼的角度來看下PoolChunk。
1、屬性和構造函數
先看下PoolChunk的屬性和構造函數
final class PoolChunk<T> {// PoolChunk會涉及到具體的內存,泛型T表示byte[](堆內存)、或java.nio.ByteBuffer(堆外內存)
final PoolArena<T> arena;//表示該PoolChunk所屬的PoolArena。
final T memory;// 具體用來表示內存;byte[]或java.nio.ByteBuffer。
final boolean unpooled;// 是否是可重用的,unpooled=false表示可重用
private final byte[] memoryMap;
private final byte[] depthMap;
private final PoolSubpage<T>[] subpages;//表示該PoolChunk所包含的PoolSubpage。也就是PoolChunk連續的可用內存。
/** Used to determine if the requested capacity is equal to or greater than pageSize. */
private final int subpageOverflowMask;
private final int pageSize;//每個PoolSubpage的大小,默認爲8192個字節(8K)
private final int pageShifts;
private final int maxOrder;
private final int chunkSize;
private final int log2ChunkSize;
private final int maxSubpageAllocs;
/** Used to mark memory as unusable */
private final byte unusable;
private int freeBytes; //當前PoolChunk空閒的內存。
PoolChunkList<T> parent;//一個PoolChunk分配後,會根據使用率掛在PoolArena的一個PoolChunkList中
// PoolChunk本身設計爲一個鏈表結構
PoolChunk<T> prev;
PoolChunk<T> next;
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize) {
unpooled = false;
this.arena = arena;
this.memory = memory;
this.pageSize = pageSize;
this.pageShifts = pageShifts;
this.maxOrder = maxOrder;
this.chunkSize = chunkSize;
unusable = (byte) (maxOrder + 1);
log2ChunkSize = log2(chunkSize);
subpageOverflowMask = ~(pageSize - 1);
freeBytes = chunkSize;
assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
maxSubpageAllocs = 1 << maxOrder;
// Generate the memory map.
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
// in each level traverse left to right and set value to the depth of subtree
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
subpages = newSubpageArray(maxSubpageAllocs);
}
PoolChunk默認情況下:maxOrder = 11,即根據maxSubpageAllocs = 1 << maxOrder
可得一個PoolChunk默認情況下由2^11=2048個SubPage構成,而默認情況下一個page默認大小爲8k,即pageSize=8K。
重點來看下memoryMap這個字段,PoolChunk中所有的PoolSubpage都放在PoolSubpage[] subpages中,爲了更好的分配,Netty用一顆平衡二叉樹記錄每個PoolSubpage的分配情況。假設PoolChunk由16個PoolSubpage構成(爲便於分析,這裏就不用默認的2048個page來進行說明chunk的結構了),那麼這些PoolSubpage將會按照如下的結構組織起來。
看上面的構造函數中的兩層for循環可以得到:從樹根到樹葉節點按每層將節點所在的(層數)依次保存在memoryMap中,即memoryMap數組中每個位置保存的是該節點所在的層數。如下就是示例的結果。
memoryMap={第0位沒用到,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4},//memoryMap數組元素長度爲 {(1<<maxOrder)>>1}}=32
memoryMap存儲二叉樹每個節點所在的層數有什麼作用呢?
對於下圖“圈出來”的節點,在memoryMap的索引爲4,其層數是2,則:
1、如果memoryMap[4] = 2,則表示其本身到下面所有的子節點都可以被分配;
2、如果memoryMap[4] = 3, 則表示該節點下有子節點已經分配過,則該節點不能直接被分配,而其子節點中的第3層還存在未分配的節點;例如:當我們請求一個大小爲4K存儲區域時就會出現這種情況
3、如果memoryMap[4] = 4,則表示該節點下的2個子節點已經被分配過(但是還存在某個子節點沒有分完),則該節點和兩個子節點不能直接被分配,而其子節點中的第4層還存在未分配的節點。例如:當我們申請一個大小爲8K和4K的存儲區域是就會出現這種情況
3、如果memoryMap[4] = 5 (即總層數 + 1), 可分配的深度已經大於總層數, 則表示該節點下的所有子節點都已經被分配。例如:當我們申請一個大小爲16K的存儲區域時就會出現這種情況
可以這麼說:如果memoryMap[i] = maxOrder+1,就表示該位置的PoolSubpage已被分配完,如果memoryMap[i] < maxOrder+1,則說明還可以分配,具體還可以分配多少,就和memoryMap[i]以及其所有子節點的值、pageSize有關。
這裏假設一個PoolSubpage的大小爲4K,如果申請一個大小爲1K的存儲區域時,該什麼辦呢?
對於小於一個Page的內存,Netty在Page中完成分配。每個Page會被切分成大小相同的多個存儲塊,存儲塊的大小由第一次申請的內存塊大小決定。對於Page的大小爲4K,第一次申請的時1K,則這個Page就會被分成4個存儲塊。
一個Page只能用於分配與第一次申請時大小相同的內存,例如,一個4K的Page,如果第一次分配了1K的內存,那麼後面這個Page就只能繼續分配1K的內存,如果有一個申請2K內存的請求,就需要在一個新的Page中進行分配。
Page中存儲區域的使用狀態通過一個long數組來維護,數組中每個long的每一位表示一個塊存儲區域的佔用情況:0表示未佔用,1表示佔用。例如:對於一個4K的Page來說如果這個Page用來分配1K的存儲與區,那麼long數組中就只有一個long類型的元素且這個數值的低4危用來指示4個存儲區域的佔用情況。
下面看看如何向PoolChunk申請一塊內存區域,allocate函數的代碼如下;
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);//分析
}
}
從上面的函數可以看到根據用戶申請的內存的大小,chunk採用了不同的方式,具體如下:
1、當需要分配的內存大於等於pageSize時,通過調用allocateRun函數實現內存分配。
2、當需要分配的內存小於pageSize時,通過調用allocateSubpage函數實現內存分配。
下面將會這兩個函數進行分析。
1、allocateSubpage(normCapacity)
先看 allocateSubpage方法,代碼如下:
private long allocateSubpage(int normCapacity) {
int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
int id = allocateNode(d);//找到符合要求的節點的索引。
if (id < 0) {
return id;
}
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;
freeBytes -= pageSize;//修改該chunk的空閒內存大小
int subpageIdx = subpageIdx(id);//求餘,得到page在subPages中的索引。
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null) {
subpage = new PoolSubpage<T>(this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(normCapacity);
}
return subpage.allocate();
}
前面提到過,當需要分配的內存小於pageSize時 ,會把一個page分割成多段,進行內存分配。因此,第一步就是要找到一個符合要求的節點。該功能由如下的allocateNode函數來完成。
private int allocateNode(int d) {
int id = 1;
int initial = - (1 << d); // has last d bits = 0 and rest all = 1
byte val = value(id);
if (val > d) { // unusable
return -1;
}
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
id <<= 1;
val = value(id);
if (val > d) {//當前節點不符合要求,需要到兄弟節點來尋找。
id ^= 1;//通過異或可以找到兄弟節點
val = value(id);
}
}
byte value = value(id);
assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
value, id & initial, d);
setValue(id, unusable); // mark as unusable
updateParentsAlloc(id);
return id;
}
簡單來說:該函數用於在二叉樹的第d層尋找一個空閒page節點,返回的是該空閒Page節點在memoryMap的索引。 此時這裏的 d = maxOrder。 代碼的具體實現思路如下:
1、從根節點開始遍歷,如果當前節點的val > d,說明存在子節點已經被分配了且剩餘節點的內存大小不夠,則此時需要到兄弟節點上繼續查找。如果當前節點爲根節點,根節點無兄弟節點,直接返回-1表示在該chunk不符合要求不能分配這麼大的內存。
2、如果當前節點的val < d,說明該節點的內存可以被分配,則通過 id <<= 1 匹配下一層直到找到value(id) = d 且id在第d層的節點;
3、分配成功的節點需要標記爲不可用,防止被再次分配,在memoryMap對應位置進行更新;
4、分配節點完成後,其父節點的狀態也需要更新,並可能引起更上一層父節點的更新。父節點的val=min{子節點val};
private void updateParentsAlloc(int id) {
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
id = parentId;
}
}
看一個例子來說明allocateNode函數中尋找節點的算法的整個過程。
首先假設PoolChunnk由16個PoolSubpage構成,每個Page的大小爲4K,那麼這些PoolSubpage將會按照如下的結構組織起來。圖中標出的數字爲:當前節點在memoryMap中的所對應的val。
現假設用戶申請一個4K的存儲空間,則會將如下圖所示用“紅橢圓”標示的page分配出去。並進行了標識,修改了相應節點在memoryMap的值。
現在假設用戶又申請一個4K的存儲空間,則具體的流經過程如下圖描述所示。最後的分配結果用“藍色橢圓”進行了標識。
分配之後,修改相應節點在memoryMap的值的結果如下圖所示。
以上幾個圖就可以很好的瞭解allocateNode函數中尋找節點的算法了。
回到allocateSubpage方法,在找到節點之後,即找到subpage之後由於申請的內存區域小於pageSize因此就開始調用subpage.allocate()來進行內存分配。具體細節將會在下篇博文講解PoolSubpage的時候進行介紹,這裏不進行介紹。
2、allocateRun(int normCapacity)
當需要分配的內存大於等於pageSize時,通過調用allocateRun函數實現內存分配。
/**
* Allocate a run of pages (>=1)
*
* @param normCapacity normalized capacity
* @return index in memoryMap
*/
private long allocateRun(int normCapacity) {
int d = maxOrder - (log2(normCapacity) - pageShifts);//
int id = allocateNode(d);
if (id < 0) {
return id;
}
freeBytes -= runLength(id);
return id;
}
該函數相比上面介紹的allocateSubpage方法類似且要簡單,這個函數主要是利用allocateNode方法來尋找符合要求的節點即可,allocateNode方法在上面有詳細的介紹。
這裏主要理解下 int d = maxOrder - (log2(normCapacity) - pageShifts);
代碼。這行代碼的作用:根據normCapacity確定需要在二叉樹的d層開始節點匹配。
還是以下面這個chunk爲例來進行說明。
假設PoolChunnk由16個PoolSubpage構成,則由(maxSubpageAllocs = 1 << maxOrder;)可以得到maxOrder=log(16)=4,而根據每個Page的大小爲4K可以得到pageShifts=12。因此假設用戶申請一個大小爲16K的緩存,則maxOrder - (log2(normCapacity) - pageShifts)=2,如下圖“紅圈”所示的那一層。
小結
看完PoolChunk,我們需要知道的東西就兩點:
1、PoolChunk是通過二叉樹的形式來組織Page的。
2、當用戶申請內存時,無論是大於等於PageSize的還是小於PageSize的第一步都是通過allocateNode方法來尋找符合要求的節點。其中小於pageSize的內存分配是在PoolSubpage上來進行分配的。
參考資料
1、http://www.jianshu.com/p/c4bd37a3555b
2、http://blog.csdn.net/prestigeding/article/details/54598967
3、《Netty權威指南》