Netty源碼分析--記憶體模型(下)(十二)
- 2019 年 10 月 3 日
- 筆記
這一節我們一起看下分配過程
1 PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) { 2 PooledByteBuf<T> buf = newByteBuf(maxCapacity); // 初始化一塊容量為 2^31 - 1的ByteBuf 3 allocate(cache, buf, reqCapacity); // reqCapacity = 1024 進入分配邏輯 4 return buf; 5 }
1 private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) { 2 final int normCapacity = normalizeCapacity(reqCapacity); // 對請求的大小進行校準,一會具體看 3 if (isTinyOrSmall(normCapacity)) { // capacity < pageSize 判斷校準之後的請求大小 是否小於 8k 4 int tableIdx; 5 PoolSubpage<T>[] table; 6 boolean tiny = isTiny(normCapacity); // 判斷請求大小是否小於512B 7 if (tiny) { // < 512 8 if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) { // 從快取中進行分配,如果是第一次這裡肯定是沒有對應大小快取的,除非之前有過釋放,然後記憶體空間被快取起來了。 9 // was able to allocate out of the cache so move on 10 return; 11 } 12 tableIdx = tinyIdx(normCapacity); // tiny數組中獲取對應的下標 13 table = tinySubpagePools; // tiny subPage數組 14 } else { // 512 <= normCapacity < 8K 15 if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) { // 一樣從快取中進行分配,這個上一節詳細的介紹過 16 // was able to allocate out of the cache so move on 17 return; 18 } 19 tableIdx = smallIdx(normCapacity); // 獲取small subPage數組下標 20 table = smallSubpagePools; // subPage 數組 21 } 22 23 final PoolSubpage<T> head = table[tableIdx];// 取得對應下標的subPage 24 25 /** 26 * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and 27 * {@link PoolChunk#free(long)} may modify the doubly linked list as well. 28 */ 29 synchronized (head) { // 鎖定防止其他操作修改head結點 30 final PoolSubpage<T> s = head.next; 31 if (s != head) {// 如果雙向鏈表已經初始化,那麼從subpage中進行分配 32 assert s.doNotDestroy && s.elemSize == normCapacity; // 斷言確保 當前的這個subPage中的elemSize大小必須和校對後的請求大小一樣 33 long handle = s.allocate(); // 從subPage中進行分配 34 assert handle >= 0; 35 s.chunk.initBufWithSubpage(buf, handle, reqCapacity); 36 incTinySmallAllocation(tiny); 37 return; 38 } 39 } 40 synchronized (this) { 41 allocateNormal(buf, reqCapacity, normCapacity); // 雙向循環鏈表還沒初始化,使用normal分配 42 } 43 44 incTinySmallAllocation(tiny); 45 return; 46 } 47 if (normCapacity <= chunkSize) { 48 if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) { 49 // was able to allocate out of the cache so move on 50 return; 51 } 52 synchronized (this) { 53 allocateNormal(buf, reqCapacity, normCapacity); 54 ++allocationsNormal; 55 } 56 } else { 57 // Huge allocations are never served via the cache so just call allocateHuge 58 allocateHuge(buf, reqCapacity); 59 } 60 }
int normalizeCapacity(int reqCapacity) { if (reqCapacity < 0) { throw new IllegalArgumentException("capacity: " + reqCapacity + " (expected: 0+)"); } if (reqCapacity >= chunkSize) { return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity); } if (!isTiny(reqCapacity)) { // 如果是 >=512 的記憶體申請,那麼就規範到2的次方大小 ,比如 1000 就會 變成 1024,自己可以把這段位操作,自己main方法試一下。 // Doubled 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; } if (directMemoryCacheAlignment > 0) { return alignCapacity(reqCapacity); } // Quantum-spaced if ((reqCapacity & 15) == 0) { // 如果 < 512 判斷如果是16的倍數,那麼直接返回 return reqCapacity; } return (reqCapacity & ~15) + 16; // 如果不是則對齊成16的倍數 }
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) { if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) || q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) || q075.allocate(buf, reqCapacity, normCapacity)) { return; } // 先嘗試從chunk鏈表中進行分配, 這裡有一個需要注意的地方 // 分配順序 先分配 q50 也就是 50% ~ 100% 的,然後是 q25 也就是 25% ~ 75% 的, 然後是 q000 0%~50% 、 qInit 0%~25% 、 q75 75% ~ 100% // Add a new chunk. PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize); // 新建一個Chunk long handle = c.allocate(normCapacity); // 記憶體分配 assert handle > 0; c.initBuf(buf, handle, reqCapacity); qInit.add(c); // 添加到初始化的chunk鏈表 }
上面的分配順序,大家想一下為什麼不是從q000開始分配呢?我找了一段分析的很好的。
在分析PoolChunkList的時候,我們知道一個chunk隨著記憶體的不停釋放,它本身會不停的往其所在的chunk list的prev list移動,直到其完全釋放後被回收。 如果這裡是從q000開始嘗試分配,雖然分配的速度可能更快了(因為分配成功的幾率更大),但一個chunk在使用率為25%以內時有更大幾率再分配,也就是一個chunk被回收的幾率大大降低了。這樣就帶來了一個問題,我們的應用在實際運行過程中會存在一個訪問高峰期,這個時候記憶體的佔用量會是平時的幾倍,因此會多分配幾倍的chunk出來,而等高峰期過去以後,由於chunk被回收的幾率降低,記憶體回收的進度就會很慢(因為沒被完全釋放,所以無法回收),記憶體就存在很大的浪費。
為什麼是從q050開始嘗試分配呢,q050是記憶體佔用50%~100%的chunk,猜測是希望能夠提高整個應用的記憶體使用率,因為這樣大部分情況下會使用q050的記憶體,這樣在記憶體使用不是很多的情況下一些利用率低(<50%)的chunk慢慢就會淘汰出去,最終被回收。然而為什麼不是從qinit中開始呢,這裡的chunk利用率低,但又不會被回收,豈不是浪費?q075,q100由於使用率高,分配成功的幾率也會更小,因此放到最後。
上面也有提到新建一個chunk,這裡我想特殊說明兩個byte數組,數組的大小是4096
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 ++; } }
循環之後的數組是怎麼樣子的呢?

MemoryMap存放分配資訊,depthMap存放節點的高度資訊 ;初始狀態時,memoryMap和depthMap相等,depthMap的值初始化後不再改變,memoryMap的值則隨著節點分配而改變。
下標代表了平衡二叉樹的節點序號,值代表該節點的高度值。

比如MemoryMap[1024] = 10, MemoryMap[2048] = 11. 好了,這個說完了,我們繼續看:
1 long allocate(int normCapacity) { 2 if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize >= 8K 3 return allocateRun(normCapacity); 4 } else { 5 return allocateSubpage(normCapacity); // < 8K 6 } 7 }
1 private long allocateSubpage(int normCapacity) { 2 // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it. 3 // This is need as we may add it back and so alter the linked-list structure. 4 PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity); // 根據請求記憶體大小,獲取下標,然後獲取對應的subPage 5 synchronized (head) { 6 int d = maxOrder; // 最大層數 11 7 int id = allocateNode(d); // 分配節點,修改涉及節點層數, 具體看下面的分析 8 if (id < 0) { // 無可分配節點 9 return id; 10 } 11 12 final PoolSubpage<T>[] subpages = this.subpages; 13 final int pageSize = this.pageSize; 14 15 freeBytes -= pageSize;// 16M(初始化值,後面會逐漸減少) - 8K 16 17 int subpageIdx = subpageIdx(id); // 2048 對應的偏移量是 0 , 2049 對應的偏移量是 1 。。。4095 對應偏移量是 2047 18 PoolSubpage<T> subpage = subpages[subpageIdx]; // 取對應下標的 subpage 19 if (subpage == null) { 20 subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity); // 創建PoolSubpage 21 subpages[subpageIdx] = subpage;
// 新建或初始化subpage並加入到chunk的subpages數組,同時將subpage加入到arena的subpage雙向鏈表中,最後完成分配請求的記憶體。
// 程式碼中,subpage != null的情況產生的原因是:subpage初始化後分配了記憶體,但一段時間後該subpage分配的記憶體釋放並從arena的雙向鏈表中刪除,
// 此時subpage不為null,當再次請求分配時,只需要調用init()將其加入到areana的雙向鏈表中即可。
22 } else { 23 subpage.init(head, normCapacity); 24 } 25 return subpage.allocate(); // 最後結果是一個long整數,其中低32位表示二叉樹中的分配的節點,高32位表示subPage中分配的具體位置 26 } 27 }
修改節點對應的層數,底下這個方法涉及了很多的位運算,這裡大家要仔細琢磨。
1 private int allocateNode(int d) { // d = 11 2 int id = 1; 3 int initial = - (1 << d); // -2048 4 byte val = value(id); // memoryMap[id] , 一般1對應的層數是 0 ,當第一個節點被分配完成後,這個節點的值會變成12 5 if (val > d) { // unusable // 如果分配完成,那麼無法進行分配,那麼就是 12 > 11 ,直接返回 -1 6 return -1; 7 } 8 while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0 9 id <<= 1; // 左移1位 10 val = value(id); // 取到節點對應的層數 11 if (val > d) { // 如果當前的值大於層數,也就是說左子節點用完了 12 id ^= 1; // 當前節點值 +1 ,取右節點 13 val = value(id); // 取右子節點 14 } 15 } 16 byte value = value(id); 17 assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d", 18 value, id & initial, d); 19 setValue(id, unusable); // mark as unusable //將該節點的值設置為12,不可用 20 updateParentsAlloc(id); // 將該節點對應的所有父節點,層數全部 + 1 21 return id; 22 }
1 private void updateParentsAlloc(int id) { 2 while (id > 1) { 3 int parentId = id >>> 1; // 取父級節點 4 byte val1 = value(id); // 5 byte val2 = value(id ^ 1); // 如果 id = 2048 那麼 id ^ 1 = 2049 , 如果 id = 2049 這裡 id ^ 1 = 2048 這裡一定要注意!!! 6 byte val = val1 < val2 ? val1 : val2; 7 setValue(parentId, val); // 修改父級對應層數 8 id = parentId; 9 } 10 }
上面這個方法很有意思,大家要仔細體會,我舉個例子

最開始是這樣的,我們開始第一次分配,那麼就是2048號節點,分配完成後會變成

這時候由於2048變成了12,不可用狀態,那麼取右節點進行分配

下一次再進行分配的時候, 發現 1024 是 12 ,那麼取右子節點, 是 1025 = 10 ,因為 10 < 11 所以再次進入循環,找到2050節點,對應層數是 11 < 12 ,則分配 2050節點分配後變成

然後依次類推。
1 private long allocateRun(int normCapacity) { 2 int d = maxOrder - (log2(normCapacity) - pageShifts); // 計算滿足需求的節點的高度 比如 16K , 那麼 就是 d= 11 - (14 - 13) = 10 3 int id = allocateNode(d); // 從第十層找空閑節點, 原理跟上面相同,就不說了 4 if (id < 0) { 5 return id; 6 } 7 freeBytes -= runLength(id); // 分配後剩餘的位元組數 8 return id; 9 }
說到這裡,記憶體模型的重點部分算是說完了。如果整個Netty系列有任何不對的地方,歡迎大家指出,我會虛心改正。
後面呢,我將打算將Spring的源碼的關鍵部分進行分析,期間也會穿插著對Redis的底層數據結構等重要內容進行分析。有需要的童鞋們,記得關注我,隨時可以獲取最新的消息。
謝謝。