前面两章分析的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[] tinySubPageHeapCaches;private final MemoryRegionCache[] smallSubPageHeapCaches;private final MemoryRegionCache[] tinySubPageDirectCaches;private final MemoryRegionCache[] smallSubPageDirectCaches;private final MemoryRegionCache[] normalHeapCaches;private final MemoryRegionCache[] 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 MemoryRegionCache[] createNormalCaches(2 int cacheSize, int maxCachedBufferCapacity, PoolArenaarea) {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[] cache = newMemoryRegionCache[arraySize];9 for (int i = 0; i < cache.length; i++) {10 cache[i] = new NormalMemoryRegionCache(cacheSize);11 }12 returncache;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, intnormCapacity, SizeClass sizeClass) {2 MemoryRegionCache> cache =cache(area, normCapacity, sizeClass);3 if (cache == null) {4 return false;5 }6 returncache.add(chunk, handle);7 }8
9 private MemoryRegionCache> cache(PoolArena> area, intnormCapacity, SizeClass sizeClass) {10 switch(sizeClass) {11 caseNormal:12 returncacheForNormal(area, normCapacity);13 caseSmall:14 returncacheForSmall(area, normCapacity);15 caseTiny:16 returncacheForTiny(area, normCapacity);17 default:18 throw newError();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, intnormCapacity) {2 int idx =PoolArena.tinyIdx(normCapacity);3 if(area.isDirect()) {4 returncache(tinySubPageDirectCaches, idx);5 }6 returncache(tinySubPageHeapCaches, idx);7 }8
9 private static MemoryRegionCache cache(MemoryRegionCache[] cache, intidx) {10 if (cache == null || idx > cache.length - 1) {11 return null;12 }13 returncache[idx];14 }
第2行, 计算数组的索引 idx = normapCapacity >> 4。
第4,6行调用的cache实现代码在9-14行。把MRC对象从数组中取出。
cacheForSmall,cacheForNormal方法和cacheForTiny类似,不同的是计算idx的方法。
1 private MemoryRegionCache> cacheForSmall(PoolArena> area, intnormCapacity) {2 int idx =PoolArena.smallIdx(normCapacity);3 if(area.isDirect()) {4 returncache(smallSubPageDirectCaches, idx);5 }6 returncache(smallSubPageHeapCaches, idx);7 }8
9 private MemoryRegionCache> cacheForNormal(PoolArena> area, intnormCapacity) {10 if(area.isDirect()) {11 int idx = log2(normCapacity >>numShiftsNormalDirect);12 returncache(normalDirectCaches, idx);13 }14 int idx = log2(normCapacity >>numShiftsNormalHeap);15 returncache(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 {@codetrue} if successful {@codefalse} otherwise3 */
4 boolean allocateTiny(PoolArena> area, PooledByteBuf> buf, int reqCapacity, intnormCapacity) {5 returnallocate(cacheForTiny(area, normCapacity), buf, reqCapacity);6 }7
8 /**
9 * Try to allocate a small buffer out of the cache. Returns {@codetrue} if successful {@codefalse} otherwise10 */
11 boolean allocateSmall(PoolArena> area, PooledByteBuf> buf, int reqCapacity, intnormCapacity) {12 returnallocate(cacheForSmall(area, normCapacity), buf, reqCapacity);13 }14
15 /**
16 * Try to allocate a small buffer out of the cache. Returns {@codetrue} if successful {@codefalse} otherwise17 */
18 boolean allocateNormal(PoolArena> area, PooledByteBuf> buf, int reqCapacity, intnormCapacity) {19 returnallocate(cacheForNormal(area, normCapacity), buf, reqCapacity);20 }
allocate方法实现比较简单,它调用MRC对象的allocate方法为PooledByteBuf分配内存,并初始化。
MemoryRegionCache(MRC)实现
PTC使用MRC对象缓存大小相同的内存块。它内部维护了一个队列,队列中保存的是大小从PoolChunk中分配的内存块。它有两个最重要的属性:
Queue> queue: 缓存内存块的队列。
SizeClass sizeClass: 内存的类型, Tiny, Small或Normal。
MRC有三个类:
MemoryRegionCache: 抽象类,定义了抽象方法initBuf。
SubPageMemoryRegionCache: 实现initBuf方法,使用Tiny或Small内存初始化PooledByteBuf。
NormalMemoryRegionCache: 实现initBuf方法,使用Normal内存初始化PooledByteBuf。
MRC的主要功能是:缓存一块内存,把PoolChunk, handle代表的内存添加到queue中。从queue中取出一块内存,调用initBuf方法初始化PooledByteBuf。
缓存内存
1 public final boolean add(PoolChunk chunk, longhandle) {2 Entry 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 returnqueued;10 }
这个方法用来吧chunk和handle代表的内存添加的queue中。Entry是MRC的内部类,实现很简单,只是为了能在queue中缓存chunk和handle数据,它使用了Recycler功能,把自己放进了可循环使用的对象池中。
从取出一块内存,并初始化PooledByteBuf
1 public final boolean allocate(PooledByteBuf buf, intreqCapacity) {2 Entry 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
2 @Override3 protected voidinitBuf(4 PoolChunk chunk, long handle, PooledByteBuf buf, intreqCapacity) {5 chunk.initBufWithSubpage(buf, handle, reqCapacity);6 }7
8 //NormalMemoryRegionCache
9 @Override10 protected voidinitBuf(11 PoolChunk chunk, long handle, PooledByteBuf buf, intreqCapacity) {12 chunk.initBuf(buf, handle, reqCapacity);13 }
由5, 12行,可以看到,这两个方法只是用来调用PoolChunk实现的PooledByteBuf初始化方法。