Netty源碼分析:PoolChunk

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權威指南》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章