前面兩章分析的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初始化方法。