netty源碼解析(4.0)-27 ByteBuf內存池:PoolArena-PoolThreadCache

  前面兩章分析的PoolChunk和PoolSubpage,從功能上來說已經可以直接拿來用了。但直接使用這個兩個類管理內存在高頻分配/釋放內存場景下會有性能問題,PoolChunk分配內存時算法複雜度最高的是allocateNode方法,釋放內存時算法複雜度最高的是free方法。 PoolChunk中二叉樹的高度是maxOrder,  那麼算法負責度是O(maxOrder),netty默認的maxOrder是11。另外,PoolChunk不是線程安全的,如果在多線程環境下需要加鎖調用,這個開銷比算法開銷還要大。

  爲了解決性能問題,netty設計PoolThreadCache(PTC)。每個線程持有一個PTC對象,每個PTC對象持有多個MemoryRegionCache(MRC)對象。MRC對象緩存了大小相同的內存塊。PooledByteBuf在釋放內存時,會把內存緩存到,MRC對象中,下次分配內存是會優先從MRC中取出緩存的內存。這樣,在高頻,多線程分配/釋放的場景下,可以避免絕大部分PoolChunk算法開銷和鎖開銷。

 

cache的設計

  在netty源碼解析(4.0)-25 ByteBuf內存池:PoolArena-PoolChunk中講到,PoolArena把內存按內存大小把內存分爲4中類型。PTC只緩存Tiny,Small, Normal三種內存。PTC內部維護了這三種內存的緩存數組,每種內存有兩個數組,分別用來緩存堆內存和直接內存。

複製代碼

    private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
    private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
    private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
    private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
    private final MemoryRegionCache<byte[]>[] normalHeapCaches;
    private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;

複製代碼

  這幾十個數組都在PTC的構造方法中初始化,tinySubPageHeapCahes和tinSubPageDirectCaches的長度,PoolArena.numTinySubpagePools。smallSubPageHeapCaches和smallSubPageDirectCaches的長度是heapArena.numSmallSubpagePools。這個兩種類型的cache都是調用createSubPageCaches方法創建。normalHeadpCaches和normalDirectCaches的長度取決於傳遞給構造方法的maxCachedBufferCapacity參數和PoolArena.pageSize,這種cache是調用createNormalCaches創建。

  PoolArena.numTinySubpagePools和PoolArena.numSmallSubpagePools的含義在netty源碼解解析(4.0)-26 ByteBuf內存池:PoolArena-PoolSubpage中有詳細的分析。

  下面以createNormalCaches方法的實現爲例分析cache的創建:

複製代碼

 1     private static <T> MemoryRegionCache<T>[] createNormalCaches(
 2             int cacheSize, int maxCachedBufferCapacity, PoolArena<T> area) {
 3         if (cacheSize > 0 && maxCachedBufferCapacity > 0) {
 4             int max = Math.min(area.chunkSize, maxCachedBufferCapacity);
 5             int arraySize = Math.max(1, log2(max / area.pageSize) + 1);
 6 
 7             @SuppressWarnings("unchecked")
 8             MemoryRegionCache<T>[] cache = new MemoryRegionCache[arraySize];
 9             for (int i = 0; i < cache.length; i++) {
10                 cache[i] = new NormalMemoryRegionCache<T>(cacheSize);
11             }
12             return cache;
13         } else {
14             return null;
15         }
16     }

複製代碼

  和createSubPageCaches不同,這個方法沒有數組長度的參數,需要自己計算數組長度。

  4,5行,計算cache數組長度。max是最大運行緩存的內存大小,它被限制爲<=chunkSize。arraySize是數組的大小。如果max/area.pageSize = 2k, (k<=maxOrder)。log2(max/ares.pageSize) = k。arraySize 最小是1, 最大是maxOrder + 1。這意味着可緩存的內存大小是pageSize * 20, paggeSize * 21, ...... pageSize * 2arraySize-1

  8-11行,創建cache數組,並逐個初始化。

  

  這三種類型的數組有不同的特性,這些特性就是它們緩存內存的方式:

  tinySubPageHeapCahes和tinSubPageDirectCaches:  這兩個數組的長度是512 >> 4 = 512/16 = 32。索引idx位置緩存的內存長度normCapacity = idx  * 16, 已知normCapacity,idx = normCapacity/16 = normCapacity >> 4。

  smallSubPageHeapCaches和smallSubPageDirectCaches: 這個數組的長度是log2(pageSize) - 9。索引idx位置緩存內存的長度normCapacity = (1 << 9) * 2idx =29+idx,  已知normCapacity,idx = log2(normCapacity) - 9。

  normalHeadpCaches和normalDirectCaches: 這個數組的長度範圍是[1, maxOrder + 1)。索引idx位置緩存的內存長度normCapacity = pageSize * 2idx, 已知normCapacity,idx=log2(normCapacity/pageSize)。

 

向cache中添加內存

  在PooledByteBuf是否內存時,會優調用PTC對象的add方法先把內存添添加到cache中:

複製代碼

 1     boolean add(PoolArena<?> area, PoolChunk chunk, long handle, int normCapacity, SizeClass sizeClass) {
 2         MemoryRegionCache<?> cache = cache(area, normCapacity, sizeClass);
 3         if (cache == null) {
 4             return false;
 5         }
 6         return cache.add(chunk, handle);
 7     }
 8 
 9     private MemoryRegionCache<?> cache(PoolArena<?> area, int normCapacity, SizeClass sizeClass) {
10         switch (sizeClass) {
11         case Normal:
12             return cacheForNormal(area, normCapacity);
13         case Small:
14             return cacheForSmall(area, normCapacity);
15         case Tiny:
16             return cacheForTiny(area, normCapacity);
17         default:
18             throw new Error();
19         }
20     }

複製代碼

  2行,調用cache方法找定位到MRC對象。

  6行,把內存添加MRC對象。

  10-19行,根據sizeClass調用不同的方法定位MRC對象。這裏的sizeClass是根據normCapacity得到的,

    normCapacity < 512: sizeClass = Tiny

    512 <= normCapacity < pageSize: sizeClass = Small

    pageSize <= normCapacity < chunkSize: sizeClass = Nomral

  接下來看看這三個用來定位MRC對象的方法是如何實現的。首先來看cacheForTiny:

複製代碼

 1     private MemoryRegionCache<?> cacheForTiny(PoolArena<?> area, int normCapacity) {
 2         int idx = PoolArena.tinyIdx(normCapacity);
 3         if (area.isDirect()) {
 4             return cache(tinySubPageDirectCaches, idx);
 5         }
 6         return cache(tinySubPageHeapCaches, idx);
 7     }
 8 
 9     private static <T> MemoryRegionCache<T> cache(MemoryRegionCache<T>[] cache, int idx) {
10         if (cache == null || idx > cache.length - 1) {
11             return null;
12         }
13         return cache[idx];
14     }

複製代碼

  第2行, 計算數組的索引 idx = normapCapacity >> 4。

  第4,6行調用的cache實現代碼在9-14行。把MRC對象從數組中取出。

 

  cacheForSmall,cacheForNormal方法和cacheForTiny類似,不同的是計算idx的方法。

複製代碼

 1     private MemoryRegionCache<?> cacheForSmall(PoolArena<?> area, int normCapacity) {
 2         int idx = PoolArena.smallIdx(normCapacity);
 3         if (area.isDirect()) {
 4             return cache(smallSubPageDirectCaches, idx);
 5         }
 6         return cache(smallSubPageHeapCaches, idx);
 7     }
 8 
 9     private MemoryRegionCache<?> cacheForNormal(PoolArena<?> area, int normCapacity) {
10         if (area.isDirect()) {
11             int idx = log2(normCapacity >> numShiftsNormalDirect);
12             return cache(normalDirectCaches, idx);
13         }
14         int idx = log2(normCapacity >> numShiftsNormalHeap);
15         return cache(normalHeapCaches, idx);
16     }

複製代碼

  第2行計算idx方法和第11行類似: log2(val),  初始化res=0,循環計算(val >>> 1) == 0 ? res : res += 1。當res不變時返回,這個是就是log2(val)的值。 

  第11行,numShiftsNormalDirect = log2(pageSize),  normCapacity >> numShiftsNormalDirect = normCapacity/pageSize。第14行同理。 

 

從cache中分配內存

  分配內存的過程也依賴前面分析的幾個cacheForXXX方法:

複製代碼

 1 /**
 2      * Try to allocate a tiny buffer out of the cache. Returns {@code true} if successful {@code false} otherwise
 3      */
 4     boolean allocateTiny(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) {
 5         return allocate(cacheForTiny(area, normCapacity), buf, reqCapacity);
 6     }
 7 
 8     /**
 9      * Try to allocate a small buffer out of the cache. Returns {@code true} if successful {@code false} otherwise
10      */
11     boolean allocateSmall(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) {
12         return allocate(cacheForSmall(area, normCapacity), buf, reqCapacity);
13     }
14 
15     /**
16      * Try to allocate a small buffer out of the cache. Returns {@code true} if successful {@code false} otherwise
17      */
18     boolean allocateNormal(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) {
19         return allocate(cacheForNormal(area, normCapacity), buf, reqCapacity);
20     }

複製代碼

  allocate方法實現比較簡單,它調用MRC對象的allocate方法爲PooledByteBuf分配內存,並初始化。

  

MemoryRegionCache(MRC)實現

  PTC使用MRC對象緩存大小相同的內存塊。它內部維護了一個隊列,隊列中保存的是大小從PoolChunk中分配的內存塊。它有兩個最重要的屬性:

  Queue<Entry<T>> queue:  緩存內存塊的隊列。

  SizeClass sizeClass:  內存的類型, Tiny, Small或Normal。

  MRC有三個類:

  MemoryRegionCache<T>: 抽象類,定義了抽象方法initBuf。

  SubPageMemoryRegionCache<T>: 實現initBuf方法,使用Tiny或Small內存初始化PooledByteBuf。

  NormalMemoryRegionCache<T>: 實現initBuf方法,使用Normal內存初始化PooledByteBuf。

  MRC的主要功能是:緩存一塊內存,把PoolChunk, handle代表的內存添加到queue中。從queue中取出一塊內存,調用initBuf方法初始化PooledByteBuf。

 

緩存內存

複製代碼

 1         public final boolean add(PoolChunk<T> chunk, long handle) {
 2             Entry<T> entry = newEntry(chunk, handle);
 3             boolean queued = queue.offer(entry);
 4             if (!queued) {
 5                 // If it was not possible to cache the chunk, immediately recycle the entry
 6                 entry.recycle();
 7             }
 8 
 9             return queued;
10         }

複製代碼

  這個方法用來吧chunk和handle代表的內存添加的queue中。Entry<T>是MRC的內部類,實現很簡單,只是爲了能在queue中緩存chunk和handle數據,它使用了Recycler功能,把自己放進了可循環使用的對象池中。

 

從取出一塊內存,並初始化PooledByteBuf

複製代碼

 1         public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) {
 2             Entry<T> entry = queue.poll();
 3             if (entry == null) {
 4                 return false;
 5             }
 6             initBuf(entry.chunk, entry.handle, buf, reqCapacity);
 7             entry.recycle();
 8 
 9             // allocations is not thread-safe which is fine as this is only called from the same thread all time.
10             ++ allocations;
11             return true;
12         }

複製代碼

  2-5行,取出一塊內存。

  6行,初始化PooledByteBuf。

  下面是兩個initBuf實現。

複製代碼

 1        //SubPageMemoryRegionCache<T>
 2         @Override
 3         protected void initBuf(
 4                 PoolChunk<T> chunk, long handle, PooledByteBuf<T> buf, int reqCapacity) {
 5             chunk.initBufWithSubpage(buf, handle, reqCapacity);
 6         } 
 7     
 8         //NormalMemoryRegionCache<T>
 9         @Override
10         protected void initBuf(
11                 PoolChunk<T> chunk, long handle, PooledByteBuf<T> buf, int reqCapacity) {
12             chunk.initBuf(buf, handle, reqCapacity);
13         }    

複製代碼

  由5, 12行,可以看到,這兩個方法只是用來調用PoolChunk實現的PooledByteBuf初始化方法。

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