Netty-真實內(nèi)存池

  1. 真實內(nèi)存指的是ByteBuf底層的array和DirectByteBuffer,真實內(nèi)存池化的好處如下:
    1.1 降低真實內(nèi)存的分配開銷。因為DirectByteBuffer的分配很抵效(創(chuàng)建堆外內(nèi)存的速度比堆內(nèi)存慢了10到20倍),特別的對于DirectByteBuffer的分配開銷降低尤為明顯。
    1.2 因為真實內(nèi)存可以復(fù)用,避免了大量對象的回收,降低了GC頻率。
  2. 怎么降低線程競爭的
    2.1 默認有核數(shù)*2個PoolArea,每個線程初始綁定一個最少線程使用的PoolArea,線程一旦綁定了對應(yīng)的PoolArea,不能再改變PoolArea。
    2.2 本地線程也有cache。

1. 總體結(jié)構(gòu)

1.1 整體結(jié)構(gòu)

首先介紹些netty內(nèi)存池的層級結(jié)構(gòu),主要分為Arena、ChunkList、Chunk、Page、Subpage這5個層級,這幾個層級的關(guān)系由大到小,如下圖所示:

image.png

Arena代表1個內(nèi)存區(qū)域,為了優(yōu)化內(nèi)存區(qū)域的并發(fā)訪問,netty中內(nèi)存池是由多個Arena組成的數(shù)組,分配時會每個線程按照輪詢策略選擇1個Arena進行內(nèi)存分配。
1個Arena由兩個PoolSubpage數(shù)組和多個ChunkList組成。兩個PoolSubpage數(shù)組分別為tinySubpagePools和smallSubpagePools。多個ChunkList按照雙向鏈表排列,每個ChunkList里包含多個Chunk,每個Chunk里包含多個Page(默認2048個),每個Page(默認大小為8k字節(jié))由多個Subpage組成。
每個Arena由如下幾個ChunkList構(gòu)成:

  • PoolChunkList<T> qInit:存儲內(nèi)存利用率0-25%的chunk
  • PoolChunkList<T> q000:存儲內(nèi)存利用率1-50%的chunk
  • PoolChunkList<T> q025:存儲內(nèi)存利用率25-75%的chunk
  • PoolChunkList<T> q050:存儲內(nèi)存利用率50-100%的chunk
  • PoolChunkList<T> q075:存儲內(nèi)存利用率75-100%的chunk
  • PoolChunkList<T> q100:存儲內(nèi)存利用率100%的chunk

每個ChunkList里包含的Chunk數(shù)量會動態(tài)變化,比如當該chunk的內(nèi)存利用率變化時會向其它ChunkList里移動。
每個Chunk里默認包含2048個Page。
每個Page包含的Subpage的大小和個數(shù)由首次從該Page分配的內(nèi)存大小決定,1個page默認大小為8k,如果首次在該page中需要分配1k字節(jié),那么該page就被分為8個Subpage,每個Subpage大小為1k。

1.2 PoolArea申請流程

在PoolArena中申請內(nèi)存的流程圖如下:

image.png
  • 對于小于pageSize大小的內(nèi)存,會在tinySubpagePools或smallSubpagePools中分配,tinySubpagePools用于分配小于512字節(jié)的內(nèi)存,smallSubpagePools用于分配大于512小于pageSize的內(nèi)存。
  • 對于大于pageSize小于chunkSize大小的內(nèi)存,會在PoolChunkList的Chunk中分配。
  • 對于大于chunkSize大小的內(nèi)存,直接創(chuàng)建非池化Chunk來分配內(nèi)存,并且該Chunk不會放在內(nèi)存池中重用。

1.3 PoolChunkList申請流程

對于在q050、q025、q000、qInit、q075這些PoolChunkList里申請內(nèi)存的流程圖如下:

  • 在PoolChunk中,數(shù)組組織呈完美二叉樹數(shù)據(jù)結(jié)構(gòu)。二叉樹葉子節(jié)點為2048個Page,每個Page的父節(jié)點用于分配pageSize*2大小內(nèi)存,同理,對于Page葉子節(jié)點的父節(jié)點的父節(jié)點,用于分配pageSize*4大小的內(nèi)存,后面以此類推。
  • 在初始狀態(tài)時,tinySubpagePools和smallSubpagePools為空,因此最初分配小于pageSize的內(nèi)存時,需要新建1個PoolChunk來分配這塊小內(nèi)存,PoolChunk會對Page分類成若干Subpage,然后用Subpage分配這塊小內(nèi)存,最后會把該Subpage放在tinySubpagePools或smallSubpagePools中。

2. 具體細節(jié)

2.1 PoolChunk

Netty一次向系統(tǒng)申請16M的連續(xù)內(nèi)存空間,這塊內(nèi)存通過PoolChunk對象包裝,為了更細粒度的管理它,進一步的把這16M內(nèi)存分成了2048個頁(pageSize=8k)。頁作為Netty內(nèi)存管理的最基本的單位 ,所有的內(nèi)存分配首先必須申請一塊空閑頁。Ps: 這里可能有一個疑問,如果申請1Byte的空間就分配一個頁是不是太浪費空間,在Netty中Page還會被細化用于專門處理小于4096Byte的空間申請 那么這些Page需要通過某種數(shù)據(jù)結(jié)構(gòu)跟算法管理起來。最簡單的是采用數(shù)組或位圖管理

image.png

如上圖1表示已申請,0表示空閑。這樣申請一個Page的復(fù)雜度為O(n),但是申請k個連續(xù)Page,就立馬退化為O(kn)。
Netty采用完全二叉樹進行管理,樹中每個葉子節(jié)點表示一個Page,即樹高為12,中間節(jié)點表示頁節(jié)點的持有者。

image.png

這樣的一個完全二叉樹可以用大小為4096的數(shù)組表示,數(shù)組元素的值含義為:

private final byte[] memoryMap; //表示完全二叉樹,共有4096個
private final byte[] depthMap; //表示節(jié)點的層高,共有4096個

  1. memoryMap[i] = depthMap[i]:表示該節(jié)點下面的所有葉子節(jié)點都可用,這是初始狀態(tài)
  2. memoryMap[i] = depthMap[i] + 1:表示該節(jié)點下面有一部分葉子節(jié)點被使用,但還有一部分葉子節(jié)點可用
  3. memoryMap[i] = maxOrder + 1 = 12:表示該節(jié)點下面的所有葉子節(jié)點不可用

有了上面的數(shù)據(jù)結(jié)構(gòu),那么頁的申請跟釋放就非常簡單了,只需要從根節(jié)點一路遍歷找到可用的節(jié)點即可,復(fù)雜度為O(lgn)。代碼為:

#PoolChunk
  //根據(jù)申請空間大小,選擇申請方法
  long allocate(int normCapacity) {
        if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
            return allocateRun(normCapacity); //大于1頁
        } else {
            return allocateSubpage(normCapacity);
        }
    }
  //按頁申請
  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;
    }
  / /申請空間,即節(jié)點編號
  private int allocateNode(int d) {
        int id = 1; //從根節(jié)點開始
        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; //左節(jié)點
            val = value(id);
            if (val > d) {
                id ^= 1; //右節(jié)點
                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);
       //更新當前申請到的節(jié)點的狀態(tài)信息
        setValue(id, unusable); // mark as unusable
       //級聯(lián)更新父節(jié)點的狀態(tài)信息
        updateParentsAlloc(id);
        return id;
    }
  //級聯(lián)更新父節(jié)點的狀態(tài)信息   
  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;
        }
    }

2.2 PoolSubpage

對于小內(nèi)存(小于4096)的分配還會將Page細化成更小的單位Subpage。Subpage按大小分有兩大類,36種情況:

  1. Tiny:小于512的情況,最小空間為16,對齊大小為16,區(qū)間為[16,512),所以共有32種情況。
  2. Small:大于等于512的情況,總共有四種,512,1024,2048,4096。
image.png
PoolSubpage中直接采用位圖管理空閑空間(因為不存在申請k個連續(xù)的空間),所以申請釋放非常簡單。
代碼:
#PoolSubpage(數(shù)據(jù)結(jié)構(gòu))
    final PoolChunk<T> chunk;   //對應(yīng)的chunk
    private final int memoryMapIdx; //chunk中那一頁,肯定大于等于2048
    private final int pageSize; //頁大小
    private final long[] bitmap; //位圖
    int elemSize; //單位大小
    private int maxNumElems; //總共有多少個單位
    private int bitmapLength; //位圖大小,maxNumElems >>> 6,一個long有64bit
    private int nextAvail; //下一個可用的單位
    private int numAvail; //還有多少個可用單位;

這里bitmap是個位圖,0表示可用,1表示不可用. nextAvail表示下一個可用單位的位圖索引,初始狀態(tài)為0,申請之后設(shè)置為-1. 只有在free后再次設(shè)置為可用的單元索引。在PoolSubpage整個空間申請的邏輯就是在找這個單元索引,只要理解了bitmap數(shù)組是個位圖,每個數(shù)組元素表示64個單元代碼的邏輯就比較清晰了

#PoolSubpage
  long allocate() {
        if (elemSize == 0) {
            return toHandle(0);
        }

        if (numAvail == 0 || !doNotDestroy) {
            return -1;
        }

        final int bitmapIdx = getNextAvail(); //查找下一個單元索引
        int q = bitmapIdx >>> 6; //轉(zhuǎn)為位圖數(shù)組索引
        int r = bitmapIdx & 63; //保留最低的8位
        assert (bitmap[q] >>> r & 1) == 0;
        bitmap[q] |= 1L << r; //設(shè)置為1

        if (-- numAvail == 0) {
            removeFromPool();
        }

        return toHandle(bitmapIdx); //對索引進行特化處理,防止與頁索引沖突
    }

  private int getNextAvail() {
        int nextAvail = this.nextAvail;
        if (nextAvail >= 0) { //大于等于0直接可用
            this.nextAvail = -1;
            return nextAvail;
        }
        return findNextAvail(); //通常走一步邏輯,只有第一次跟free后nextAvail才可用
    }
  //找到位圖數(shù)組可用單元,是一個long類型,有[1,64]單元可用
  private int findNextAvail() {
        final long[] bitmap = this.bitmap;
        final int bitmapLength = this.bitmapLength;
        for (int i = 0; i < bitmapLength; i ++) {
            long bits = bitmap[i];
            if (~bits != 0) {
                return findNextAvail0(i, bits);
            }
        }
        return -1;
    }
   //在64的bit中找到一個可用的
    private int findNextAvail0(int i, long bits) {
        final int maxNumElems = this.maxNumElems;
        final int baseVal = i << 6;

        for (int j = 0; j < 64; j ++) {
            if ((bits & 1) == 0) {
                int val = baseVal | j;
                if (val < maxNumElems) {
                    return val;
                } else {
                    break;
                }
            }
            bits >>>= 1;
        }
        return -1;
    }

2.3 PoolSubpage池

第一次申請小內(nèi)存空間的時候,需要先申請一個空閑頁,然后將該頁轉(zhuǎn)成PoolSubpage,再將該頁設(shè)為已被占用,最后再把這個PoolSubpage存到PoolSubpage池中。這樣下次就不需要再去申請空閑頁了,直接去池中找就好了。Netty中有36種PoolSubpage,所以用36個PoolSubpage鏈表表示PoolSubpage池。

#PoolArena
private final PoolSubpage<T>[] tinySubpagePools;
private final PoolSubpage<T>[] smallSubpagePools;

#PoolSubpage
PoolSubpage<T> prev;
PoolSubpage<T> next;

image.png
#PoolArena
allocate(...reqCapacity...){
   final int normCapacity = normalizeCapacity(reqCapacity);
   //找到池的類型跟下標
   boolean tiny = isTiny(normCapacity);
   if (tiny) { // < 512
       tableIdx = tinyIdx(normCapacity);
       table = tinySubpagePools;
    } else {
       tableIdx = smallIdx(normCapacity);
       table = smallSubpagePools;
    }
    final PoolSubpage<T> head = table[tableIdx];
    synchronized (head) {
        final PoolSubpage<T> s = head.next;
        if (s != head) {
            //通過PoolSubpage申請
            long handle = s.allocate();
            ...
        }
    }
}

2.4 PoolChunkList

上面討論了PoolChunk的內(nèi)存分配算法,但是PoolChunk只有16M,這遠遠不夠用,所以會很很多很多PoolChunk,這些PoolChunk組成一個鏈表,然后用PoolChunkList持有這個鏈表

#PoolChunkList
private PoolChunk<T> head;

#PoolChunk
PoolChunk<T> prev;
PoolChunk<T> next;

image.png

這里還沒這么簡單,它有6個PoolChunkList,所以將PoolChunk按內(nèi)存使用率分類組成6個PoolChunkList,同時每個PoolChunkList還把各自串起來,形成一個PoolChunkList鏈表。

#PoolChunkList
 private final int minUsage; //最小使用率
 private final int maxUsage; //最大使用率
 private final int maxCapacity;

private PoolChunkList<T> prevList;
private final PoolChunkList<T> nextList;

#PoolArena
//[100,) 每個PoolChunk使用率100%
q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
//[75,100) 每個PoolChunk使用率75-100%
q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
//[50,100)
q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
//[25,75)
q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
//[1,50)
q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);

image.png

既然按使用率分配,那么PoolChunk在使用過程中是會動態(tài)變化的,所以PoolChunk會在不同PoolChunkList中變化。同時申請空間,使用哪一個PoolChunkList也是有先后順序的

#PoolChunkList
  boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
        if (head == null || normCapacity > maxCapacity) {
            return false;
        }
        for (PoolChunk<T> cur = head;;) {
            long handle = cur.allocate(normCapacity);
            if (handle < 0) {
                cur = cur.next;
                if (cur == null) {
                    return false;
                }
            } else {
                cur.initBuf(buf, handle, reqCapacity);
                if (cur.usage() >= maxUsage) {
                    remove(cur); 
                    nextList.add(cur); //移到下一個PoolChunkList中
                }
                return true;
            }
        }
    }

#PoolArena
allocateNormal(...){
  if (q050.allocate(...) || q025.allocate(...) ||
            q000.allocate(...) || qInit.allocate(...) ||
            q075.allocate(...)) {
            return;
        }
  PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
  ...
  qInit.add(c);
}

這樣設(shè)計的目的是考慮到隨著內(nèi)存的申請與釋放,PoolChunk的內(nèi)存碎片也會相應(yīng)的升高,使用率越高的PoolChunk其申請一塊連續(xù)空間的失敗的概率也會大大的提高。

  1. qInit前置節(jié)點為自己,且minUsage=Integer.MIN_VALUE,意味著一個初分配的chunk,在最開始的內(nèi)存分配過程中(內(nèi)存使用率<25%),即使完全釋放也不會被回收,會始終保留在內(nèi)存中。
  2. q000沒有前置節(jié)點,當一個chunk進入到q000列表,如果其內(nèi)存被完全釋放,則不再保留在內(nèi)存中,其分配的內(nèi)存被完全回收。
  3. 各個PoolChunkList的區(qū)間是交叉的,這是故意的,因為如果介于一個臨界值的話,PoolChunk會在前后PoolChunkList不停的來回移動。
  4. 為什么area中ChunkList的分配順序如下面代碼所示,要從q050開始?
    4.1 為什么不從q0000開始?
    因為永遠從空閑率很高的chunk分配,那么chunk的空閑率就不太可能降為0,chunk自然不會被回收,造成內(nèi)存得到釋放。當負載很大申請了很多的chunk,但是負載降低時chunk又不能及時回收。
    4.2 為什么不從qinit開始?
    因為qinit得不到釋放。(為什么?????)
    4.3 為什么不從q075和q100開始?
    q075和q100由于內(nèi)存利用率太高,導(dǎo)致內(nèi)存分配的成功率大大降低,因此放到最后;
if (q050.allocate(...) || q025.allocate(...) ||
            q000.allocate(...) || qInit.allocate(...) ||
            q075.allocate(...)) {
            return;
        }
 

2.5 PoolArena

PoolArena是上述功能的門面,通過PoolArena提供接口供上層使用,屏蔽底層實現(xiàn)細節(jié)。為了減少線程成間的競爭,很自然會提供多個PoolArena。Netty默認會生成2×CPU個PoolArena跟IO線程數(shù)一致。然后第一次使用的時候會找一個使用線程最少的PoolArena

     private <T> PoolArena<T> leastUsedArena(PoolArena<T>[] arenas) {
            if (arenas == null || arenas.length == 0) {
                return null;
            }

            PoolArena<T> minArena = arenas[0];
            for (int i = 1; i < arenas.length; i++) {
                PoolArena<T> arena = arenas[i];
                if (arena.numThreadCaches.get() < minArena.numThreadCaches.get()) {
                    minArena = arena;
                }
            }

            return minArena;
        }

2.6 本地線程存儲

雖然提供了多個PoolArena減少線程間的競爭,但是難免還是會存在鎖競爭,所以需要利用ThreaLocal進一步優(yōu)化,把已申請的內(nèi)存放入到ThreaLocal自然就沒有競爭了。大體思路是在ThreadLocal里面放一個PoolThreadCache對象,然后釋放的內(nèi)存都放入到PoolThreadCache里面,下次申請先從PoolThreadCache獲取。
但是,如果thread1申請了一塊內(nèi)存,然后傳到thread2在線程釋放,這個Netty在內(nèi)存holder對象里面會引用PoolThreadCache,所以還是會釋放到thread1里


image.png

3. 性能測試

可以寫兩個簡單的測試用例,感受一下Netty內(nèi)存池帶來的效果。

  1. 申請10000000個HeapBuffer,DirectBuffer,池化的DirectBuffer花的時間, 可以看出池化效果非常明顯,而且十分平和
capacity HeapBuffer DirectBuffer 池化的DirectBuffer
64Byte 465 13211 2059
256Byte 946 15074 2309
512Byte 2528 19516 2188
1024Byte 4393 21928 2044
  1. 啟一個DiscardServer,然后發(fā)送80G的數(shù)據(jù),看下GC次數(shù),效果感人
非池化 池化 池化+COMPOSITE_CUMULATOR
208 27 0

參考

Netty內(nèi)存池實現(xiàn)
Netty-內(nèi)存管理
Netty 4 at Twitter: Reduced GC Overhead

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容