PooledByteBufAllocator是Netty中比较复杂的一种ByteBufAllocator , 因为他涉及到对内存的缓存,分配和释放策略,PooledByteBufAllocator,顾名思义,是对内存做了池化,也就是缓存一定容量的内存,每次用PooledByteBufBufAllocator申请ByteBuf 时,不需要重新向操作系统或者JVM申请内存,而是可以直接从预先申请好的内存中取一块内存类似于线程池, 因此在需要频繁申请和释放内存的场景下,PooledByteBufAllocator比UnpooledByteBufAllocator 性能明显更好。
- PooledByteBufAllocator概述
为了保证线程安全以及减少不同的线程在申请ByteBuf时的竞争,PooledByteBufAllocator 为每个不同的线程都缓存了一些内存。
PooledByteBufAllocator分配ByteBuf主要为两个步骤 。
- 拿到线程局部缓存PoolThreadCache,PoolThreadCache保存在一个ThreadLocal上,因此每个线程对应一个PoolThreadCache 。
- 在PoolThreadCache进行内存分配。
- 如果该线程的PoolThreadCache上没有足够的内存可供分配,则在线程局部缓存的Arena上进行内存分配 。
以分配HeapByteBuf为例(分配)
以分配HeapByteBuf为例(分配DirectByteBuf的逻辑是一样的),由于PooledByteBufAllocator有可能被多个线程同时使用,因此PooledByteBufAllocator内部有一个PoolThreadLocalCache和一个PooledArena数组,每一个PoolThreadCache和PoolArena中缓存了一些内存,PoolThreadCache和线程是一一对应的,但PoolArena和线程是一对多的关系,某个线程需要分配 ByteBuf是先从自己PoolThreadCache 上分配,分配不到,再到PoolArena中去取。
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { // 拿到该线程对应的PoolThreadCache PoolThreadCache cache = threadCache.get(); // 拿到该PoolThreadCache对应的PoolArena PoolArena<byte[]> heapArena = cache.heapArena; final ByteBuf buf; if (heapArena != null) { // 从cache或heapArena上分配一个ByteBuf buf = heapArena.allocate(cache, initialCapacity, maxCapacity); } else { buf = PlatformDependent.hasUnsafe() ? new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) : new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity); } return toLeakAwareBuffer(buf); }
我们可以画出Thread, PoolThreadCache和PoolArena 之间的关系 。
二,PoolThreadCache的结构
PoolThreadCache中缓存的内存是以MemoryRegionCache数组的形式存在的,而一个MemoryRegionCache中又包含了一个Entry队列,每一个Entry可以代表一个特定大小的内存,一个MemoryRegionCache的所有Entry 所代表的内存大小是一样的。 MemoryRegionCache的结构如下 :
MemoryRegionCache的SizeClass是一个枚举类型,表示他的Entry的内存的大小范围:Tiny(0~496B),Small(512B ~ 4KB),Normal(8K ~ 16M),而size则表示每个Entry的具体的内存大小,值得注意的是,大于32K 且小于16M 的内存块不是在PoolThreadCache中进行缓存的。
PoolThreadCache中有三个MemoryRegionCache数组,tinySubPageHeapCaches,smallSubPageHeapCache和normalSubPageHeapCaches,分别对应三种不同的SizeClass
(directCaches的原理与heapCaches是一样的,这里就以heapCaches为例子,这三个数组结构如下)
数组中的每一个节点都是一个MemoryRegionCache,其中包含了一组特定的内存大小的Entry,例如,如果向PooledByteBufAllocator申请一个20B大小的HeapByteBuf,PooledByteBufAllocator会从当前线程的PoolThreadCache的tinySubPageHeapCaches数组中找到一个size为32B的那个MemoryRegionCache(PooledByteBufAllocator会将20B 整化为32B ) , 再从他的queue中取一个Entry返回给申请者。
三. PoolArena 的结构
PooledByteBufAllocator有三个重要的数据结构tinySubpagePools,smallSubpagePools和chunkList。
- ChunkList和Chunk
ChunkList
PoolArena每次向操作系统申请直接内存或向JVM 申请堆内存时,都是以Chunk为单位申请的,Chunk默认的大小为16M , 一个Chunk 又包含2048个Page, 每个Page 大小为 16M / 2048 = 8 KB , 一个ChunkList包含一个Chunk 的双向列表,PoolArena中又有5个ChunkList,且这5个ChunkList又组成了一个双向列表,这5个ChunkList分别为qinit, q000,q025,q050,q075,q100。
这5个ChunkList有什么区别呢? 每个ChunkList的名字后面的数字表示这个ChunkList所维护的Chunk的使用率,qinit维护的Chunk的使用率为0~25%, q000为1% ~ 50% , q025 为 25% ~ 75 % ,q050 为 50 % ~ 100% ,q075 为75% ~ 100% , q100为 100% 。
例如:如果一个Chunk 是新申请的,那么他的使用率为0,它会在qinit的ChunkList中,之后某个线程占用了这个Chunk的100个Page, 他的使用率变成了 100 * 8 * 1024 B /16M ≈ 5 % 。
为什么这5个ChunkList的使用率范围会有重叠呢 ? 这是为了防止Chunk的使用率频率变化而导致他频繁切换ChunkList,例如 q025 中的一个Chunk, 当它的使用率从60%变为30%,它不会切换到q000中,而是继续在q025中。
Chunk 中用了一个平衡二叉树来表示它的内存使用状况,树中的一个节点代表某一范围的内存,同一深度的所有节点代表的内存大小是相同的,并且用一个byte数组memoryMap表示所有节点的状态,节点的状态反映它所代表这段内存的使用情况,节点的状态有三种情况 。
- 如果这段内存全部被使用,节点的状态值为12,最大深度 + 1
- 如果这段内存部分被使用,节点的状态值为他所在的深度 + 1
- 如果这段内存完全被使用,节点状态的值为他所在的深度。
每次申请内存时,都会根据申请的大小从对应的深度中开始查找,例如,如果申请一个4MB 的内存,就会从d = 2 开始查找(因为深度为2的Node都是大小为4M的内存) 。
- 先从Node4开始,如果Node4的状态值为2, 那么表示 0 ~ 4 M这一块内存完全未被使用,就会将 0 ~ 4M 这一块内存返回给申请者,并修改相应的节点的状态: Node4 的状态值为12(表示 0 ~ 4M这一块内存已经全部被使用),Node2 的状态值改为2 (Node2 所在的深度值 + 1 ),Node1 的状态值为1(因为0 ~ 8M 和 0 ~ 16M 只是部分被使用) 。
- 如果Node4的状态值为3或者12,表示 0 ~ 4M 这一块内存已经部分被完成使用了,则继续查看Node5的状态值 。
- 如果Node4的状态值为3或者12,表示0~4M这一块内存已经部分或完全被使用了,则继续查看Node5的状态 。
- 如果d=2的节点状态值都不为2 ,表示Chunk 已经不存在连续的4M 大小的内存了, 则申请失败,继续从ChunkList的下一个Chunk中去申请 。
2. tinySubpagePools和smallSubpagePools
Chunk 分配内存时是以Page(size为8K)为单位,很多时候这个size还是太大了, 如果申请一个1K的内存,Chunk 返回的是一个8K 的Page ,这样属实是有点浪费了, 因此PoolArena中还有两个类型为PoolSubpage的数组,tinySubpagePools和smallSubpagePools。
tinySubpagePools和smallSubpagePools的结构与上面的讲解PoolThreadCache中所提到的tinySubPageCaches,smallSubPageCaches的结构类似,所不同的是数组的类型是一个PoolSubpage,一个是MemoryRegionCache, 且tinySubpagePools和smallSubpagePools中相同大小的PoolSubpage是以双向链表的形式连接在一起,而不是队列,每个双向链表的head节点都是一个虚拟节点,如图所示 :
每个PoolSubpage都是由某个Chunk的某个Page 转换而来的,例如,如果向PoolArena申请32B大小的内存,那么PoolArena会从ChunList 中选出一个Chunk,然后将这个Chunk 的某个Page转换为PoolSubpage ,再将这个PoolSubpage加入到tinySubPagePools数组中,每个Chunk都有一个长度为2048的数组,用来表示某个Page是否转换为PoolSubpage 。
PoolSubpage[] subpages
例如,如果subpage[2]不为null,表明第2个Page(对应的二叉树的Node2050)已经转换为了PoolSubpage,即16 ~ 24 这一段内存是按subpage分配的, 如果subpage[0] == null , 表示第0个Page (对应的二叉树 Node204 )依是一个正常的Page 。
我们可以画出整个tinySubpagePools数组,Chunk ,Page,Subpage之间的关系图,如图所示 :
四 ByteBuf 回收
由于一开始每个线程的PoolThreadCache中都没有缓存任何内存的,所以,一开始申请的内存时必然是从PoolArena中去申请,PoolArena会根据选择向操作系统 直接内存或JVM 堆内存申请新的内存来new 一个Chunk 或者从ChunkList中选一个已有的Chunk 来进行分配,这一点我们前面已经分析过 。
当某个线程调用release()方法去释放他所持有的一个ByteBuf 时,通常不会直接释放给PoolArena,而是将这块内存放入自己的PoolThreadCache中的tinySubPageCaches或smallSubPageCaches或normalSubpageCaches的数组中找到之前被释放的那块内存重新使用,而对于PoolArena来说,一块内存是一直被该线程占用的,处于已经使用状态,只有在以下几种情况下才会将内存归还给PoolArena 。
线程已经结束,那么该线程的PoolThreadCache会将所有的缓存的内存归还给PoolArena,这样其他线程就可以在PoolArena上申请这些内存了。
释放的内存块大于32K, PoolThreadCache最大只缓存32K 的内存块,因此如果release一个大于32K 的内存, 会直接归还给PoolArena,相应的MemoryRegionCache的队列长度达到最大值,再release对应的大小的内存块时,不会加入到MemoryRegionCache的队列上去,而是直接归还给PoolArena。
上面的这些都是基础的理论知识,下面从例子出发,来分析Netty 的内存管理到底是如何实现。
public final void testAllocation() { final PooledByteBufAllocator allocator = new PooledByteBufAllocator( true, // preferDirect 0, // nHeapArena 1, // nDirectArena 8192, // pageSize 11, // maxOrder 0, // tinyCacheSize 0, // smallCacheSize 0, // normalCacheSize true // useCacheForAllThreads ); // create tiny buffer final ByteBuf b1 = allocator.directBuffer(24); // create small buffer final ByteBuf b2 = allocator.directBuffer(800); // create normal buffer final ByteBuf b3 = allocator.directBuffer(8192 * 2); }
没有什么好说的,先进入PooledByteBufAllocator的构造方法 。
public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectArena, int pageSize, int maxOrder, int tinyCacheSize, int smallCacheSize, int normalCacheSize, boolean useCacheForAllThreads) { this(preferDirect, nHeapArena, nDirectArena, pageSize, maxOrder, tinyCacheSize, smallCacheSize, normalCacheSize, useCacheForAllThreads, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT); }
在进入实际的PooledByteBufAllocator构造方法之前,先来看静态常量的初始化 。
static { int defaultPageSize = SystemPropertyUtil.getInt("io.netty.allocator.pageSize", 8192); // page的大小 Throwable pageSizeFallbackCause = null; try { validateAndCalculatePageShifts(defaultPageSize); } catch (Throwable t) { pageSizeFallbackCause = t; defaultPageSize = 8192; } DEFAULT_PAGE_SIZE = defaultPageSize; // 一个chunk的大小=pageSize << maxOrder int defaultMaxOrder = SystemPropertyUtil.getInt("io.netty.allocator.maxOrder", 11); Throwable maxOrderFallbackCause = null; try { validateAndCalculateChunkSize(DEFAULT_PAGE_SIZE, defaultMaxOrder); } catch (Throwable t) { maxOrderFallbackCause = t; defaultMaxOrder = 11; } DEFAULT_MAX_ORDER = defaultMaxOrder; // Determine reasonable default for nHeapArena and nDirectArena. // Assuming each arena has 3 chunks, the pool should not consume more than 50% of max memory. final Runtime runtime = Runtime.getRuntime(); /* * We use 2 * available processors by default to reduce contention as we use 2 * available processors for the * number of EventLoops in NIO and EPOLL as well. If we choose a smaller number we will run into hot spots as * allocation and de-allocation needs to be synchronized on the PoolArena. * * See https://github.com/netty/netty/issues/3888. */ final int defaultMinNumArena = NettyRuntime.availableProcessors() * 2; final int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER; // 8192 << 11 DEFAULT_NUM_HEAP_ARENA = Math.max(0, // heap arena的个数,min(cpu核数,maxMemory/chunkSize/6),一般来说会=cpu核数 SystemPropertyUtil.getInt( "io.netty.allocator.numHeapArenas", (int) Math.min( defaultMinNumArena, runtime.maxMemory() / defaultChunkSize / 2 / 3))); DEFAULT_NUM_DIRECT_ARENA = Math.max(0, // direct arena的个数, min(cpu核数,directMemory/chunkSize/6),一般来说会=cpu核数 SystemPropertyUtil.getInt( "io.netty.allocator.numDirectArenas", (int) Math.min( defaultMinNumArena, PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3))); // cache sizes // PoolThreadCache中tiny cache每个MemoryRegionCache中的Entry个数 DEFAULT_TINY_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.tinyCacheSize", 512); // PoolThreadCache中small cache每个MemoryRegionCache中的Entry个数 DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.smallCacheSize", 256); // PoolThreadCache中normal cache每个MemoryRegionCache中的Entry个数 DEFAULT_NORMAL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.normalCacheSize", 64); // 32 kb is the default maximum capacity of the cached buffer. Similar to what is explained in // 'Scalable memory allocation using jemalloc' PoolThreadCache中normal cache数组长度 DEFAULT_MAX_CACHED_BUFFER_CAPACITY = SystemPropertyUtil.getInt( "io.netty.allocator.maxCachedBufferCapacity", 32 * 1024); // the number of threshold of allocations when cached entries will be freed up if not frequently used // PoolThreadCache中的cache收缩阈值,每隔该值次数,会进行一次收缩 DEFAULT_CACHE_TRIM_INTERVAL = SystemPropertyUtil.getInt( "io.netty.allocator.cacheTrimInterval", 8192); DEFAULT_CACHE_TRIM_INTERVAL_MILLIS = SystemPropertyUtil.getLong( "io.netty.allocation.cacheTrimIntervalMillis", 0); DEFAULT_USE_CACHE_FOR_ALL_THREADS = SystemPropertyUtil.getBoolean( "io.netty.allocator.useCacheForAllThreads", true); DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT = SystemPropertyUtil.getInt( "io.netty.allocator.directMemoryCacheAlignment", 0); // Use 1023 by default as we use an ArrayDeque as backing storage which will then allocate an internal array // of 1024 elements. Otherwise we would allocate 2048 and only use 1024 which is wasteful. DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK = SystemPropertyUtil.getInt( "io.netty.allocator.maxCachedByteBuffersPerChunk", 1023); if (logger.isDebugEnabled()) { logger.debug("-Dio.netty.allocator.numHeapArenas: {}", DEFAULT_NUM_HEAP_ARENA); logger.debug("-Dio.netty.allocator.numDirectArenas: {}", DEFAULT_NUM_DIRECT_ARENA); if (pageSizeFallbackCause == null) { logger.debug("-Dio.netty.allocator.pageSize: {}", DEFAULT_PAGE_SIZE); } else { logger.debug("-Dio.netty.allocator.pageSize: {}", DEFAULT_PAGE_SIZE, pageSizeFallbackCause); } if (maxOrderFallbackCause == null) { logger.debug("-Dio.netty.allocator.maxOrder: {}", DEFAULT_MAX_ORDER); } else { logger.debug("-Dio.netty.allocator.maxOrder: {}", DEFAULT_MAX_ORDER, maxOrderFallbackCause); } logger.debug("-Dio.netty.allocator.chunkSize: {}", DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER); logger.debug("-Dio.netty.allocator.tinyCacheSize: {}", DEFAULT_TINY_CACHE_SIZE); logger.debug("-Dio.netty.allocator.smallCacheSize: {}", DEFAULT_SMALL_CACHE_SIZE); logger.debug("-Dio.netty.allocator.normalCacheSize: {}", DEFAULT_NORMAL_CACHE_SIZE); logger.debug("-Dio.netty.allocator.maxCachedBufferCapacity: {}", DEFAULT_MAX_CACHED_BUFFER_CAPACITY); logger.debug("-Dio.netty.allocator.cacheTrimInterval: {}", DEFAULT_CACHE_TRIM_INTERVAL); logger.debug("-Dio.netty.allocator.cacheTrimIntervalMillis: {}", DEFAULT_CACHE_TRIM_INTERVAL_MILLIS); logger.debug("-Dio.netty.allocator.useCacheForAllThreads: {}", DEFAULT_USE_CACHE_FOR_ALL_THREADS); logger.debug("-Dio.netty.allocator.maxCachedByteBuffersPerChunk: {}", DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK); } }
这些参数都加了注释,关于这些参数如何使用,在使用到时,再来分析。接下来进入真实的PooledByteBufAllocator的初始化方法 。
public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectArena, int pageSize, int maxOrder, int tinyCacheSize, int smallCacheSize, int normalCacheSize, boolean useCacheForAllThreads, int directMemoryCacheAlignment) { super(preferDirect); threadCache = new PoolThreadLocalCache(useCacheForAllThreads); this.tinyCacheSize = tinyCacheSize; // 默认值512 this.smallCacheSize = smallCacheSize; // 默认值256 this.normalCacheSize = normalCacheSize; // 默认值64 // 2 ^ 13 = 8192 , 2 ^ 24 = 16777216 // 16M = 16 * 1024 kb = 16 * 1024 * 1024 b = 2 ^ 4 * 2 ^ 10 * 2 ^ 10 = 2 ^ 24 = 16777216 // 因此一个 chunkSize 为 16 M // 默认 pageSize = 8192 = 8 ^ 1024 = 8 kb = 2 ^ 13 // 因此一个chunkSize = 2 ^ 24 / 2 ^ 13 = 2 ^ 11 个pageSize // validateAndCalculateChunkSize()方法,较验 maxOrder 的值不能大于14 ,并且 chunkSize 不能大于1024MB // 当然,默认maxOrder的值为11, chunkSize = 16 MB chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder); // 较验nHeapArena和nDirectArena的值不能小于0 checkPositiveOrZero(nHeapArena, "nHeapArena"); checkPositiveOrZero(nDirectArena, "nDirectArena"); // 较验directMemoryCacheAlignment的值不能小于0 checkPositiveOrZero(directMemoryCacheAlignment, "directMemoryCacheAlignment"); // directMemoryCacheAlignment 大于0 ,但平台不支持unSafe ,则抛出异常 if (directMemoryCacheAlignment > 0 && !isDirectMemoryCacheAlignmentSupported()) { throw new IllegalArgumentException("directMemoryCacheAlignment is not supported"); } // 如果directMemoryCacheAlignment不是0,也不是2的幂次方,则抛出异常 if ((directMemoryCacheAlignment & -directMemoryCacheAlignment) != directMemoryCacheAlignment) { throw new IllegalArgumentException("directMemoryCacheAlignment: " + directMemoryCacheAlignment + " (expected: power of two)"); } // 默认 pageSize = 8192 = 8 ^ 1024 = 8 kb, pageShifts = 13 // 8192 的二进制数为 0000 0000 0000 0000 0010 0000 0000 0000 // 8192 = 1 << 13 ,pageShifts 值就是1左移的位数,当然 // validateAndCalculatePageShifts()方法还较验了 pageSize 不能小于4196 // 并且pageSize也必须为2的幂次方 int pageShifts = validateAndCalculatePageShifts(pageSize); if (nHeapArena > 0) { // heap arena的个数 // min(cpu核数,maxMemory/chunkSize/6),一般来说会=cpu核数 heapArenas = newArenaArray(nHeapArena); List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(heapArenas.length); for (int i = 0; i < heapArenas.length; i ++) { PoolArena.HeapArena arena = new PoolArena.HeapArena(this, pageSize, maxOrder, pageShifts, chunkSize, directMemoryCacheAlignment); heapArenas[i] = arena; metrics.add(arena); } heapArenaMetrics = Collections.unmodifiableList(metrics); } else { heapArenas = null; heapArenaMetrics = Collections.emptyList(); } // direct arena的个数 , min(cpu核数,directMemory/chunkSize/6),一般来说会=cpu核数 if (nDirectArena > 0) { directArenas = newArenaArray(nDirectArena); List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(directArenas.length); for (int i = 0; i < directArenas.length; i ++) { PoolArena.DirectArena arena = new PoolArena.DirectArena( this, pageSize, maxOrder, pageShifts, chunkSize, directMemoryCacheAlignment); directArenas[i] = arena; metrics.add(arena); } directArenaMetrics = Collections.unmodifiableList(metrics); } else { directArenas = null; directArenaMetrics = Collections.emptyList(); } metric = new PooledByteBufAllocatorMetric(this); }
static final class HeapArena extends PoolArena<byte[]> { HeapArena(PooledByteBufAllocator parent, int pageSize, int maxOrder, int pageShifts, int chunkSize, int directMemoryCacheAlignment) { super(parent, pageSize, maxOrder, pageShifts, chunkSize, directMemoryCacheAlignment); } ... } protected PoolArena(PooledByteBufAllocator parent, int pageSize, int maxOrder, int pageShifts, int chunkSize, int cacheAlignment) { // 从PooledByteBufAllocator中传送过来的相关字段值。 this.parent = parent; this.pageSize = pageSize; //page的大小,默认8K this.maxOrder = maxOrder; //表示chunk中由Page节点构成的二叉树的最大高度。默认11 this.pageShifts = pageShifts; //pageShifts=log(pageSize),默认13 this.chunkSize = chunkSize; //chunk的大小 directMemoryCacheAlignment = cacheAlignment; directMemoryCacheAlignmentMask = cacheAlignment - 1; subpageOverflowMask = ~(pageSize - 1); //该变量用于判断申请的内存大小与page之间的关系,是大于,还是小于 tinySubpagePools = newSubpagePoolArray(numTinySubpagePools); // tinySubpagePools来缓存(或说是存储)用来分配tiny(小于512)内存的Page; for (int i = 0; i < tinySubpagePools.length; i ++) { tinySubpagePools[i] = newSubpagePoolHead(pageSize); } numSmallSubpagePools = pageShifts - 9; // 为什么是-9 ,因为512 = 2 ^ 9 smallSubpagePools = newSubpagePoolArray(numSmallSubpagePools); // smallSubpagePools来缓存用来分配small(大于等于512且小于pageSize)内存的Page for (int i = 0; i < smallSubpagePools.length; i ++) { smallSubpagePools[i] = newSubpagePoolHead(pageSize); } q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize); q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize); q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize); q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize); q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize); qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize); q100.prevList(q075); q075.prevList(q050); q050.prevList(q025); q025.prevList(q000); q000.prevList(null); qInit.prevList(qInit); List<PoolChunkListMetric> metrics = new ArrayList<PoolChunkListMetric>(6); metrics.add(qInit); metrics.add(q000); metrics.add(q025); metrics.add(q050); metrics.add(q075); metrics.add(q100); chunkListMetrics = Collections.unmodifiableList(metrics); }
请看基于上图的内存池模型,Netty 抽象出一些核心组件,如 PoolArena、PoolChunk、PoolChunkList、PoolSubpage、PoolThreadCache、MemoryRegionCache 关系图。
PoolArena
Netty采用固定的多个Arena进行内存分配,Arena 的默认数量与CPU核数有关, 通过创建多个Arena来缓解资源的竞争,从而提高内存的分配效率,线程在首次申请分配内存时,会通过round-robin的方式轮询Arena数组,选择一个固定的Arena 在线程的生命周期内只有与该Arena打交道,所以每个线程都保存了Arena信息,从而提高访问效率。
PoolArena的数组结构如下 :
- 包含两个PoolSubpage数组和六个PoolChunkList,两个PoolSubpage数组分别存放Tiny 和 Small类型的内存块,六个PoolChunkList分别存储不同的利用率的Chunk ,构成一个双向循环链表。
- PoolArena 对应实现了Subpage和Chunk 中的内存配置, 其中PoolSubpage用于分配小于8K 的内存, PoolChunkList用于分配 大于8K 的内存。
PoolSubpage
PoolSubpage分为Tiny和Small 两种规格,对应tinySubpagePools和smallSubpagePools两个数组,对于Tiny 规格,内存单位最小为16B , 按16B 依次递增,共32种情况,对于Small规格,共分为512B, 1024B , 2048B , 4096B 四种情况,分别对应两个数组的长度大小 。
PoolChunkList
PoolChunkList用于Chunk场景的内存分配,PoolArena初始化了6个PoolChunkList, 分别为qInit,q000, q025 ,q050 ,q075 ,q100,类似于jemalloc 中的run 队列,代表不同的内存使用率。
- qInit, 内存使用率为0 ~ 25% 的Chunk 。
- q000: 内存使用率为 1 ~ 50% 的Chunk 。
- q025 ,内存使用率为 25% ~ 75% 的Chunk 。
- q050 , 内存使用率为 50% ~ 100% 的Chunk。
- q075, 内存使用率为 75% ~ 100% 的Chunk 。
- q100 , 内存使用率为 100% 的Chunk。
除了qInit,剩余的PoolChunkList构成双向链表,随着Chunk 内存使用率的变化,Netty会重新检查的使用率并放入对应的PoolChunkList,所以PoolChunk会在不同的PoolChunkList移动 。
PoolChunkList还有以下几个注意的点需要解释
- qInit用于存储初始化分配的PoolChunk, 因为在第一次内存分配时, PoolChunkList中并没有可用的PoolChunk,所以需要创建一个PoolChunk并添加到qInit列表中, qInit中的PoolChunk 即使内存被完全释放也不会被回收,避免PoolChunk重复的初始化工作 。
- q000则用于存放内存使用率为1 ~ 50% 的PoolChunk ,q000中的PoolChunk内存被完全释放后,PoolChunk从链表中移除,对应分配的内存也会被回收。
再来看一下PooledByteBufAllocator的结构。
protected AbstractByteBufAllocator(boolean preferDirect) { directByDefault = preferDirect && PlatformDependent.hasUnsafe(); emptyBuf = new EmptyByteBuf(this); }
默认创建PooledByteBufAllocator后,PooledByteBufAllocator有了哪些内容呢?
接下来看直接内存分配方法directBuffer()。
public ByteBuf directBuffer(int initialCapacity) { // DEFAULT_MAX_CAPACITY为Integer.MAX_VALUE return directBuffer(initialCapacity, DEFAULT_MAX_CAPACITY); } public ByteBuf directBuffer(int initialCapacity, int maxCapacity) { if (initialCapacity == 0 && maxCapacity == 0) { return emptyBuf; } // initialCapacity小于0 或 initialCapacity的值大于2047MB ,则抛出异常 validate(initialCapacity, maxCapacity); return newDirectBuffer(initialCapacity, maxCapacity); } public ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { PoolThreadCache cache = threadCache.get(); PoolArena<ByteBuffer> directArena = cache.directArena; final ByteBuf buf; if (directArena != null) { buf = directArena.allocate(cache, initialCapacity, maxCapacity); } else { buf = PlatformDependent.hasUnsafe() ? UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) : new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity); } return toLeakAwareBuffer(buf); }
大家可能很容易忽略上面加粗这一行代码PoolThreadCache cache = threadCache.get(),像我们获取实体类的get()方法一样,不就是返回实体的一个属性不? 如果这样想,你就错了,threadCache是谁?threadCache可是PoolThreadLocalCache,而看一下PoolThreadLocalCache的类结构 。 PoolThreadLocalCache可是继承了FastThreadLocal。
而FastThreadLocal是Netty自己开发的一套性能上更加优于ThreadLocal的线程范围内变量共享的类。 之前也专门写了一篇博客关于ThreadLocal和FastThreadLocal之间的对比博客,有兴趣可以自己去看看,如果还不明白,把FastThreadLocal当成ThreadLocal来看就可以了。先看FastThreadLocal的get()方法实现。
public final V get() { InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); Object v = threadLocalMap.indexedVariable(index); // 如果PoolThreadCache已经初始化过了,则直接返回 if (v != InternalThreadLocalMap.UNSET) { return (V) v; } return initialize(threadLocalMap); }
如果PoolThreadCache初始化过了,就直接返回,如果没有初始化,则进入PoolThreadCache的initialize()方法,看其如何实现。
FastThreadLocal#initialize方法 private V initialize(InternalThreadLocalMap threadLocalMap) { V v = null; try { v = initialValue(); } catch (Exception e) { PlatformDependent.throwException(e); } threadLocalMap.setIndexedVariable(index, v); addToVariablesToRemove(threadLocalMap, this); return v; }
每一个线程都对应一个index,在threadLocalMap.get()方法时,会用index去threadLocalMap中取对应槽位上的值,而 threadLocalMap.setIndexedVariable(index, v),就是将初始化的值设置到对应的槽位上,下次get()方法时,就能取到对应的值了,就不需要再次调用initialize()方法了。
final class PoolThreadLocalCache extends FastThreadLocal<PoolThreadCache> { private final boolean useCacheForAllThreads; PoolThreadLocalCache(boolean useCacheForAllThreads) { this.useCacheForAllThreads = useCacheForAllThreads; } @Override protected synchronized PoolThreadCache initialValue() { // 因为在初始化PooledByteBufAllocator时,会指定 // heapArenas 和 directArenas数组大小 , 而leastUsedArena() 和 leastUsedArena()方法 // 找出heapArenas和directArenas 中,使用量最小的PoolArena final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas); final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas); final Thread current = Thread.currentThread(); // 如果useCacheForAllThreads为true或 当前线程是FastThreadLocalThread if (useCacheForAllThreads || current instanceof FastThreadLocalThread) { final PoolThreadCache cache = new PoolThreadCache( heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize, DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL); if (DEFAULT_CACHE_TRIM_INTERVAL_MILLIS > 0) { final EventExecutor executor = ThreadExecutorMap.currentExecutor(); if (executor != null) { executor.scheduleAtFixedRate(trimTask, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, TimeUnit.MILLISECONDS); } } return cache; } // No caching so just use 0 as sizes. return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0); } } 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]; // 采用轮询的方式,取出PoolThreadCache引用最少的PoolArena 返回 if (arena.numThreadCaches.get() < minArena.numThreadCaches.get()) { minArena = arena; } } return minArena; }
上述代码,先分析DEFAULT_CACHE_TRIM_INTERVAL_MILLIS 大于0的情况。 不过这要结合我的另外一篇博客 Netty 源码解析(上) 来分析,我们知道,很多的任务都是由EventLoop 创建的线程来执行的。
final EventExecutor executor = ThreadExecutorMap.currentExecutor(); if (executor != null) { executor.scheduleAtFixedRate(trimTask, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, TimeUnit.MILLISECONDS); }
而每个线程都会创建一个EventExecutor,如果线程创建了EventExecutor,并且DEFAULT_CACHE_TRIM_INTERVAL_MILLIS大于0的情况。此时会创建一个定时任务添加到scheduledTaskQueue队列中。
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { ObjectUtil.checkNotNull(command, "command"); ObjectUtil.checkNotNull(unit, "unit"); if (initialDelay < 0) { throw new IllegalArgumentException( String.format("initialDelay: %d (expected: >= 0)", initialDelay)); } if (period <= 0) { throw new IllegalArgumentException( String.format("period: %d (expected: > 0)", period)); } validateScheduled0(initialDelay, unit); validateScheduled0(period, unit); return schedule(new ScheduledFutureTask<Void>( this, Executors.<Void>callable(command, null), ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period))); } <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) { if (inEventLoop()) { scheduledTaskQueue().add(task); } else { execute(new Runnable() { @Override public void run() { scheduledTaskQueue().add(task); } }); } return task; } public PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue() { if (scheduledTaskQueue == null) { scheduledTaskQueue = new DefaultPriorityQueue<ScheduledFutureTask<?>>( SCHEDULED_FUTURE_TASK_COMPARATOR, // Use same initial capacity as java.util.PriorityQueue 11); } return scheduledTaskQueue; }
DefaultPriorityQueue它又是什么呢? 从名字上来看,不就是优先级队列吗?
再看ScheduledFutureTask的结构
ScheduledFutureTask又继承了延迟接口。
其实之前也写过一篇博客,ArrayBlockingQueue&LinkedBlockingQueue&DelayQueue&SynchronousQueue&PriorityBlockingQueue源码解析 ,有兴趣可以去看看。在那篇博客中举了一个很有趣的例子,如果想在Java中用HashMap来实现redis缓存失效的功能,用的就是PriorityQueue队列。在这里也是一样。 在创建ScheduledFutureTask任务时,指定了过期时间,如果时间还没有到,是无法从scheduledTaskQueue取得任务的,如果时间到了。 则能从scheduledTaskQueue队列中取得任务,而又是在哪里获取scheduledTaskQueue队列中任务的呢?以NioEventLoop为例 。在其select(boolean oldWakenUp)方法中,有一个hasScheduledTasks()方法。如下图所示 。
hasScheduledTasks()方法的实现如下 。
protected final boolean hasScheduledTasks() { Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue; ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek(); return scheduledTask != null && scheduledTask.deadlineNanos() <= nanoTime(); }
从scheduledTaskQueue队列中获取任务,如果能获取任务,证明过期时间到了,则会执行后面的runAllTasks()方法中执行相应的定时任务
因此最终执行下面的任务
private final Runnable trimTask = new Runnable() { @Override public void run() { PooledByteBufAllocator.this.trimCurrentThreadCache(); } }; public boolean trimCurrentThreadCache() { PoolThreadCache cache = threadCache.getIfExists(); if (cache != null) { cache.trim(); return true; } return false; } void trim() { trim(tinySubPageDirectCaches); trim(smallSubPageDirectCaches); trim(normalDirectCaches); trim(tinySubPageHeapCaches); trim(smallSubPageHeapCaches); trim(normalHeapCaches); } private static void trim(MemoryRegionCache<?>[] caches) { if (caches == null) { return; } for (MemoryRegionCache<?> c: caches) { trim(c); } } private static void trim(MemoryRegionCache<?> cache) { if (cache == null) { return; } cache.trim(); } public final void trim() { int free = size - allocations; allocations = 0; // We not even allocated all the number that are if (free > 0) { free(free, false); } }
最终调用了关键方法free()来释放内存,当然内存的释放后面再来具体分析 。我相信对DEFAULT_CACHE_TRIM_INTERVAL_MILLIS大于0的情况有了大致的了解后,继续来看initialValue()方法中PoolThreadCache的初始化 。
PoolThreadCache(PoolArena<byte[]> heapArena, PoolArena<ByteBuffer> directArena, int tinyCacheSize, int smallCacheSize, int normalCacheSize, int maxCachedBufferCapacity, int freeSweepAllocationThreshold) { // 检测maxCachedBufferCapacity是否小于0 checkPositiveOrZero(maxCachedBufferCapacity, "maxCachedBufferCapacity"); this.freeSweepAllocationThreshold = freeSweepAllocationThreshold; this.heapArena = heapArena; this.directArena = directArena; if (directArena != null) { // numTinySubpagePools = 512 >>> 4 实际上就是 512 / 16 = 32,因为Tiny的最小内存单元为16B // 因此 16B,32B,48B,64B,80B,96B,112B,128B...496B 总的内存规格为 512 / 16 = 32 种内存规格 tinySubPageDirectCaches = createSubPageCaches( tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny); // 如果pageSize = 8192B , 则Small的内存规格为 512B,1024B,2048B,4096B 4种内存规格 // 因此 numSmallSubpagePools的计算规则为 = log2(8192) - log2(512) = log2(2 ^ 13 ) - log2(2 ^ 9 ) = 13 - 9 = 4 smallSubPageDirectCaches = createSubPageCaches( smallCacheSize, directArena.numSmallSubpagePools, SizeClass.Small); // pageSize = 8192 = 2 ^ 13 // numShiftsNormalDirect = log(2 ^ 13 ) = 13 numShiftsNormalDirect = log2(directArena.pageSize); normalDirectCaches = createNormalCaches( normalCacheSize, maxCachedBufferCapacity, directArena); directArena.numThreadCaches.getAndIncrement(); } else { // No directArea is configured so just null out all caches tinySubPageDirectCaches = null; smallSubPageDirectCaches = null; normalDirectCaches = null; numShiftsNormalDirect = -1; } if (heapArena != null) { // Create the caches for the heap allocations tinySubPageHeapCaches = createSubPageCaches( tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny); smallSubPageHeapCaches = createSubPageCaches( smallCacheSize, heapArena.numSmallSubpagePools, SizeClass.Small); numShiftsNormalHeap = log2(heapArena.pageSize); normalHeapCaches = createNormalCaches( normalCacheSize, maxCachedBufferCapacity, heapArena); heapArena.numThreadCaches.getAndIncrement(); } else { // No heapArea is configured so just null out all caches tinySubPageHeapCaches = null; smallSubPageHeapCaches = null; normalHeapCaches = null; numShiftsNormalHeap = -1; } // Only check if there are caches in use. if ((tinySubPageDirectCaches != null || smallSubPageDirectCaches != null || normalDirectCaches != null || tinySubPageHeapCaches != null || smallSubPageHeapCaches != null || normalHeapCaches != null) && freeSweepAllocationThreshold < 1) { throw new IllegalArgumentException("freeSweepAllocationThreshold: " + freeSweepAllocationThreshold + " (expected: > 0)"); } }
在看懂上面代码之前,先来了解一下内存规格。
Tiny代表0 ~ 512B 之间的内存块,Small 代表512B ~ 8K 之间的内存块,Normal代表8K ~ 16M 的内存块,Huge 代表大于16M 的内存块,在Netty 中定义了一个SizeClass类型的枚举,用于描述上图中的内存规格类型,分别为Tiny,Small 和Normal ,但图中的Huge,并未在代码中定义,当分配大于16M 时,可以归类为Huge 场景,Netty 会直接使用非池化的方式进行内存分配。
- Chunk : Netty 向操作系统申请的内存单位,所有的内存分配操作也是基于Chunk 完成的,Chunk可以理解为Page的集合,Chunk 默认大小为16M 。
- Page : Chunk 用于管理内存的单位,Netty 中的Page 大小为8K, 不要与Linux中的内存页Page 想混淆了, 假如我们需要分配64K 的内存,需要在Chunk 中选取8个Page进行内存分配 。
- Subpage: 用于Page 的内存分配,当分配的内存大小远小于Page ,直接分配一个Page 会造成严重的内存浪费,所以需要将Page划分为多个相同的子块进行分配,这里的子块就相当于Subpage,照Tiny和Small两种规格,Subpage的大小也会分为两种情况,在Tiny 场景下,最小的划分单位为16B ,按16B依次递增,16B , 32B, 48B, … 496B ,总共32种内存规格 , 在Small场景下,总共可以划分为512B, 1024B, 2048B, 4096B 四种情况, Subpage没有固定的大小,需要根据用户分配的缓冲区的大小决定, 例如分配1K 的内存时, Netty 会把一个Page 等分为8个1K 的Subpage 。
private static <T> MemoryRegionCache<T>[] createSubPageCaches( int cacheSize, int numCaches, SizeClass sizeClass) { if (cacheSize > 0 && numCaches > 0) { @SuppressWarnings("unchecked") MemoryRegionCache<T>[] cache = new MemoryRegionCache[numCaches]; for (int i = 0; i < cache.length; i++) { // TODO: maybe use cacheSize / cache.length cache[i] = new SubPageMemoryRegionCache<T>(cacheSize, sizeClass); } return cache; } else { return null; } } private static final class SubPageMemoryRegionCache<T> extends MemoryRegionCache<T> { SubPageMemoryRegionCache(int size, SizeClass sizeClass) { super(size, sizeClass); } @Override protected void initBuf( PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, PooledByteBuf<T> buf, int reqCapacity) { chunk.initBufWithSubpage(buf, nioBuffer, handle, reqCapacity); } } private abstract static class MemoryRegionCache<T> { private final int size; private final Queue<Entry<T>> queue; private final SizeClass sizeClass; private int allocations; MemoryRegionCache(int size, SizeClass sizeClass) { this.size = MathUtil.safeFindNextPositivePowerOfTwo(size); queue = PlatformDependent.newFixedMpscQueue(this.size); this.sizeClass = sizeClass; } ... }
private static <T> MemoryRegionCache<T>[] createNormalCaches( int cacheSize, int maxCachedBufferCapacity, PoolArena<T> area) { if (cacheSize > 0 && maxCachedBufferCapacity > 0) { // area.chunkSize为16M, maxCachedBufferCapacity为 PoolThreadCache中normalCache数组长度且默认值为 32 * 1024 // 也就是说, maxCachedBufferCapacity的默认最大值为 32K // 16M = 2 ^ 24 // 32 * 1024 = 2 ^ 5 * 2 ^ 10 = 2 ^ 15 // 因此二者最小值为 2 ^ 15 int max = Math.min(area.chunkSize, maxCachedBufferCapacity); // log2(2 ^ 15 / 2 ^ 13 ) = log2( 2 ^ 2 ) = 2 // arraySize = 2 + 1 = 3 // arraySize 可以理解为 从8K 开始 ,到maxCachedBufferCapacity 之间(32K ),以2的倍数递增 // 总共有多少种内存规格,显然只有8K,16K ,32K 三种内存规格 int arraySize = Math.max(1, log2(max / area.pageSize) + 1); @SuppressWarnings("unchecked") MemoryRegionCache<T>[] cache = new MemoryRegionCache[arraySize]; for (int i = 0; i < cache.length; i++) { cache[i] = new NormalMemoryRegionCache<T>(cacheSize); } return cache; } else { return null; } } private static final class NormalMemoryRegionCache<T> extends MemoryRegionCache<T> { NormalMemoryRegionCache(int size) { super(size, SizeClass.Normal); } @Override protected void initBuf( PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, PooledByteBuf<T> buf, int reqCapacity) { chunk.initBuf(buf, nioBuffer, handle, reqCapacity); } }
PoolThreadCache中有一个重要的数据结构,MemoryRegionCache,MemoryRegionCache有三个重要的属性,分别为Queue, sizeClass 和size , 下图是不同的内存规格所对应的MemoryRegionCache属性取值范围 。
MemoryRegionCache 实际上就是一个队列,当内存释放时, 将内存块加入队列中, 下次再分配同样的规格的内存时,直接从队列中取出空闲的内存块。
PoolThreadCache将不同规格大小的内存都使用单独的MemoryRegionCache维护,如下图所示,图中每个节点都对应一个MemoryRegionCache,例如Tiny场景下对应32种内存规格会使用32个MemoryRegionCahce维护,所以PoolThreadCache源码中Tiny,Small , Normal 类型的MemoryRegionCache 数组长度分别为32,4,3 。
接下来继续回头看newDirectBuffer()方法,在newDirectBuffer()方法中有一行PoolArena<ByteBuffer> directArena = cache.directArena;,因为之后要调用directArena的allocate()方法,那directArena从何而来呢?请看 initialValue()方法,最终调用leastUsedArena()方法,找出被PoolThreadCache 引用数最少的PoolArena传入PoolThreadCache中,而PoolArena又来源于PooledByteBufAllocator的初始化 。
接着看buf = directArena.allocate(cache, initialCapacity, maxCapacity);这一行代码 。
PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) { PooledByteBuf<T> buf = newByteBuf(maxCapacity); allocate(cache, buf, reqCapacity); return buf; } private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) { final int normCapacity = normalizeCapacity(reqCapacity); // 先把请求内存优化成内存池中的标准单元格大小,具体详细注解在后面 if (isTinyOrSmall(normCapacity)) { // capacity < pageSize int tableIdx; PoolSubpage<T>[] table; boolean tiny = isTiny(normCapacity); if (tiny) { // 如果 < 512 if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) { // 先尝试从线程本地缓存中获取 // was able to allocate out of the cache so move on return; } // 通过空间大小获取 tinySubpagePools 的下标,由于tinySubpagePools存储的是16的倍数的PoolSubpage,因此normCapacity/16=tableIndx tableIdx = tinyIdx(normCapacity); table = tinySubpagePools; } else { // 大于或等于512且小于8192 if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) { // was able to allocate out of the cache so move on return; } tableIdx = smallIdx(normCapacity); // 通过空间大小获取smallSubpagePools的下标 table = smallSubpagePools; } final PoolSubpage<T> head = table[tableIdx]; /** * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and * {@link PoolChunk#free(long)} may modify the doubly linked list as well. */ synchronized (head) { // 对head 头指针加锁 final PoolSubpage<T> s = head.next; if (s != head) { assert s.doNotDestroy && s.elemSize == normCapacity;//当头部指针与其next不同时,则表示此PoolSubpages缓存中有内存可分配 long handle = s.allocate(); assert handle >= 0; // 判断是否分配成功 s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity); // 初始化PoolByteBuf incTinySmallAllocation(tiny); // 增加对应的分配的次数 return; } } synchronized (this) { // 为PoolArena加锁 allocateNormal(buf, reqCapacity, normCapacity); // 若线程本地缓存和PoolSubpages中没有可分配的内存,此分配方法详细注释在后面 } incTinySmallAllocation(tiny); return; } if (normCapacity <= chunkSize) { if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) { // 尝试从线程本地缓存中获取 // was able to allocate out of the cache so move on return; } synchronized (this) { allocateNormal(buf, reqCapacity, normCapacity); ++allocationsNormal; } } else { // Huge allocations are never served via the cache so just call allocateHuge allocateHuge(buf, reqCapacity); // 大内存分配时不放入到缓存池 } } // capacity < pageSize, 假如 pageSize = 8192 ,8192 对应的二进制数为 0000 0000 0000 0000 0010 0000 0000 0000 // subpageOverflowMask = - 8192 ,对应的二进制数为 1111 1111 1111 1111 1110 0000 0000 0000 // 如果normCapacity & ~ pageSize == 0 ,假如 pageSize = 8192 // 那么 normCapacity 的值一定是小于8192的值 boolean isTinyOrSmall(int normCapacity) { return (normCapacity & subpageOverflowMask) == 0; }
从源码中可以看出在分配Tiny, Small 和Normal类型的内存时,都会尝试从PoolThreadCache中进行分配 。
- 对申请的内存大小做向上取整,例如20B的内存大小会取整为32B
- 当申请的内存大小小于8K时,分为Tiny和Small两种情况,分别都会优先尝试从PoolThtreadCache 中分配内存,如果PoolThreadCache分配失败,才会走PoolArena的分配流程。
- 当申请的内存大小大于8K , 但是小于Chunk的默认大小16M ,属于Normal的内存分配,也会优先尝试从PoolThreadCache分配内存,如果PoolThreadCache分配失败,才会走PoolArena的分配流程。
- 当申请的内存大小大于16M时,则不会走PoolThreadCache ,直接进行分配 。
先来看内存规格的计算方法 。
public int normalizeCapacity(int reqCapacity) { // 内存被划分成固定大小的内存单元,会根据请求的内存进行计算匹配最接过的内存单元 checkPositiveOrZero(reqCapacity, "reqCapacity"); //检查reqCapacity 是否小于0,则抛出异常 if (reqCapacity >= chunkSize) { return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity); } // (normCapacity & 0xFFFFFE00) == 0 // 0xFFFFFE00 二进制为 1111 1111 1111 1111 1111 1110 0000 0000 // 512 对应的二进制数为 0000 0000 0000 0000 0000 0010 0000 0000 // 如果一个数 & 0xFFFFFE00 == 0 ,则这个数一定是一个小于512的数 ,因此 !isTiny(reqCapacity),则reqCapacity 一定大于512 if (!isTiny(reqCapacity)) { // >= 512 // 大于 512 // Doubled int normalizedCapacity = reqCapacity; // 防止 reqCapacity为512 ,1024 ,2048 等临界点翻倍,先进行减1的操作 // 例如 当reqCapacity为512时,先减1变成511,再寻找与其最接过的2幂数,下面位置或操作主要是让其所有的位都变成1 ,当reqCapacity为 // (512,1023)时,经过经过下面的位移或操作后,其拥有的所有的01位都变成了1,即变成了1024,最后再加1就成了1024 // 由于reqCapacity为整数,最多32位,因此处的右移为(1,1 * 2 , 2 * 2 ,2 * 2 * 2 ,2 * 2 * 2 * 2 ) normalizedCapacity --; normalizedCapacity |= normalizedCapacity >>> 1; normalizedCapacity |= normalizedCapacity >>> 2; normalizedCapacity |= normalizedCapacity >>> 4; normalizedCapacity |= normalizedCapacity >>> 8; normalizedCapacity |= normalizedCapacity >>> 16; normalizedCapacity ++; // 当溢出时会变成负数,此时需要右移1位 if (normalizedCapacity < 0) { normalizedCapacity >>>= 1; } assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0; // 如果reqCapacity的值并不是2的幂次方,则返回大于reqCapacity并且最接近reqCapacity的2的幂次方值 return normalizedCapacity; } // 如果 directMemoryCacheAlignment的值大于0 ,且是16的整数倍 if (directMemoryCacheAlignment > 0) { // 假如directMemoryCacheAlignment=64 ,则 // 如果reqCapacity是directMemoryCacheAlignment的整数倍,则直接返回 // 如果不是directMemoryCacheAlignment的整数倍, // 则返回大于reqCapacity且距离reqCapacity最近且是directMemoryCacheAlignment的整数倍的数, // 如 57 => 64 = 64 * 1 // 127 => 128 = 64 * 2 // 151 = 192 = 64 * 3 return alignCapacity(reqCapacity); } // Quantum-spaced // 15 对应的二进制数为 0000 0000 0000 0000 0000 0000 0000 1111 // 如果 reqCapacity & 15==0,如果reqCapacity已经是16的倍数,则返回reqCapacity自身 if ((reqCapacity & 15) == 0) { // 当小于512且是16的整数倍时,直接返回 return reqCapacity; } // 可以写个程序测试 : // for (int i = 0; i < 1026; i++) { // int result = (i & ~15) + 16; // int result2 = (i / 16 )*16 + 16 ; // System.out.println("i = " + i + ",result=" + result + ", result/16 = " + (result / 16)); // } // 结果输出 : // i = 493,result=496, result/16 = 31 // i = 494,result=496, result/16 = 31 // i = 495,result=496, result/16 = 31 // i = 511,result=512, result/16 = 32 // i = 510,result=512, result/16 = 32 // i = 511,result=512, result/16 = 32 // ~15 = 1111 1111 1111 1111 1111 1111 1111 0000 // 511 = 0000 0000 0000 0000 0000 0001 1111 1111 // 如果不是16的整数倍时,低4位变成16,也就是任何数都会变成16的倍数 // 如果不是16的整数倍,则转化成一个大于reqCapacity,且与reqCapacity最接近的并且是16的整数倍的数 // 可以看成是 (reqCapacity & ~15) + 16 等价于 ((int)(reqCapacity / 16)) * 16 + 16, return (reqCapacity & ~15) + 16; }
上述方法写了这么多,其实就是内存规格的计算方法,如果申请的内存小于512,则计算出的值一定是大于等于请求申请内存,且距离reqCapacity最近并且是16的倍数的值,如果大于512,则返回的是值大于等于请求的值,且是距离请求值最近的512 * 2 的幂次方,因此可以总结得出结论。 三种内存规格 。
第一种:16B,32B,48B,64B,80B,96B,112B,128B,144B,160B,176B,192B,208B,224B,240B,256B,272B,288B,304B,320B,336B,352B,368B,384B,400B,416B,432B,448B,464B,480B,496B
第二种:
512B,1K,2K,4K
第三种
8K,16K,32K
如果申请的内存是上面的三种内存规格中的任意一个值,则直接返回该值,如果不是,则找到大于申请内存,且距申请内存最近的值,如果申请内存为32B,则直接返回32B,如果申请的内存为95B,则返回96B,如申请的内存为9K,则返回16K。
boolean allocateTiny(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) { return allocate(cacheForTiny(area, normCapacity), buf, reqCapacity); }
在上述方法中,有一个cacheForTiny()方法很有意思。
private MemoryRegionCache<?> cacheForTiny(PoolArena<?> area, int normCapacity) { int idx = PoolArena.tinyIdx(normCapacity); if (area.isDirect()) { return cache(tinySubPageDirectCaches, idx); } return cache(tinySubPageHeapCaches, idx); } static int tinyIdx(int normCapacity) { return normCapacity >>> 4; // 相当于除以16 } private static <T> MemoryRegionCache<T> cache(MemoryRegionCache<T>[] cache, int idx) { if (cache == null || idx > cache.length - 1) { return null; } return cache[idx]; }
那cacheForTiny()这个方法是什么意思呢? 先来看一个图。
所以tinyIdx() 这个方法的主要意图获取所申请内存规格的数组下标,而cacheForTiny()这个方法的意图就是获取申请内存规格所对应数组下标的头节点。 接下来继续看allocate()方法 。
private boolean allocate(MemoryRegionCache<?> cache, PooledByteBuf buf, int reqCapacity) { // 如果本地内存中对应的内存规格头节点为空,肯定没有可用节点,本地内存分配失败 if (cache == null) { // no cache found so just return false here return false; } // 默认每执行8192次allocate(), 就会调用一次trim()进行内存整理 boolean allocated = cache.allocate(buf, reqCapacity); if (++ allocations >= freeSweepAllocationThreshold) { allocations = 0; trim(); } return allocated; }
如果本地内存内存规格对应的头节点不为空,可能有可用节点,则进入本地内存分配方法 。
public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) { // 从队列中获取空闲内存块 Entry<T> entry = queue.poll(); // 如果队列中没有空闲内存块 if (entry == null) { return false; } initBuf(entry.chunk, entry.nioBuffer, entry.handle, buf, reqCapacity); entry.recycle(); // allocations is not thread-safe which is fine as this is only called from the same thread all time. ++ allocations; return true; }
关于initBuf()方法,我们后面再来分析,接下来继续看allocateSmall()方法 。
boolean allocateSmall(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) { return allocate(cacheForSmall(area, normCapacity), buf, reqCapacity); } private MemoryRegionCache<?> cacheForSmall(PoolArena<?> area, int normCapacity) { int idx = PoolArena.smallIdx(normCapacity); if (area.isDirect()) { return cache(smallSubPageDirectCaches, idx); } return cache(smallSubPageHeapCaches, idx); } static int smallIdx(int normCapacity) { int tableIdx = 0; int i = normCapacity >>> 10; while (i != 0) { i >>>= 1; tableIdx ++; } return tableIdx; }
我们之前知道small的内存规格有512B,1024B,2048B ,4096B这四种规格,而smallIdx()方法是计算当前申请的内存在smallSubPagePools的索引位置,如,你申请了1024B的内存,那你申请的内存将分配到如下图所示索引为1的PoolSubpage位置 。
同样的道理,如果在PoolThreadCache中申请不到内存,则会从PoolArena中分配内存。
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) { ... /** * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and * {@link PoolChunk#free(long)} may modify the doubly linked list as well. */ synchronized (head) { // 对head 头指针加锁 final PoolSubpage<T> s = head.next; if (s != head) { assert s.doNotDestroy && s.elemSize == normCapacity; //当头部指针与其next不同时,则表示此PoolSubpages缓存中有内存可分配 long handle = s.allocate(); assert handle >= 0; // 判断是否分配成功 s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity); // 初始化PoolByteBuf incTinySmallAllocation(tiny); // 增加对应的分配的次数 return; } } //可能会存在多个PoolThreadCache共用一个PoolArena,因此这里需要对PoolArena加锁 synchronized (this) { allocateNormal(buf, reqCapacity, normCapacity); // 若线程本地缓存和PoolSubpages中没有可分配的内存,此分配方法详细注释在后面 } ... }
Netty 中负责线程分配的组件有两个:PoolArena和PoolThreadCache。PoolArena 是多个线程共享的,每个线程会固定绑定一个 PoolArena,PoolThreadCache 是每个线程私有的缓存空间,如下图所示。
在上面的代码中又分为两种情况,如果head.next = head 的情况,则表示此PoolSubpages缓存中没有内存可分配,则需要调用allocateNormal(buf, reqCapacity, normCapacity); 方法进行内存分配,如果有可分配的内存,则直接调用s.allocate();分配内存即可。 接下来进入allocateNormal()方法。
看到这里,我们再次回顾一下之前PoolArena的结构 。
PoolChunkList
PoolChunkList 用于 Chunk 场景下的内存分配,PoolArena初始化了6个 PoolChunkList,分别为 qInit、q000、q025、q050、q075、q100,类似于jemalloc 中 run 队列,代表不同的内存使用率:
- qInit,内存使用率为 0 ~ 25% 的 Chunk。
- q000,内存使用率为 1 ~ 50% 的 Chunk。
- q025,内存使用率为 25% ~ 75% 的 Chunk。
- q050,内存使用率为 50% ~ 100% 的 Chunk。
- q075,内存使用率为 75% ~ 100% 的 Chunk。
- q100,内存使用率为 100% 的 Chunk。
除了qInit,剩余的PoolChunkList构成双向链表。随着 Chunk 内存使用率的变化,Netty 会重新检查内存的使用率并放入对应的 PoolChunkList,所以 PoolChunk 会在不同的 PoolChunkList 移动。
PoolChunkList还有以下几个注意点需要解释 。
注意点1: PoolChunkList中的qInit和q000内存使用率接近,为什么要设计为两个而不合并呢?
- qInit 用于存储初始化分配的PoolChunk,因为第一次内存分配时, PoolChunkList中并没有可用的PoolChunk,所以需要新建一个PoolChunk并添加到qInit列表中,qInit中的PoolChunk即使内存被完全释放也不会回收,避免PoolChunk的重复初始化工作 。
- q000则用于存放内存使用率为1~50%的PoolChunk,q000中的PoolChunk内存被完全释放后,PoolChunk从链表中移除,对应的内存也会被回收。
注意点2 : 在分配大于8K的内存时,链表的访问顺序是q50->q025->q000->qInit->q075,对应的源码如下
// Method must be called inside synchronized(this) { ... } block // 内存分配 private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) { // 先从Q050链表开始分配内存,从链表中循环取出PoolChunk,如果分配成功了,则返回true, 否则返回false if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) || q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) || q075.allocate(buf, reqCapacity, normCapacity)) { return; } // Add a new chunk. 5 个链表开始分配内存,此时需要开辟一块新的PoolChun内存 PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize); boolean success = c.allocate(buf, reqCapacity, normCapacity); // 此处为PoolChunk内存分配,在前面小节中详细讲解过 assert success; qInit.add(c); // 分配成功后,把PoolChunk追加到qInit链表中 }
在频繁的分配的场景下,如果从q000开始,会有大部分的PoolChunk面临频繁的创建和销毁,造成内存分配性能降低,如果从q050开始,会使PoolChunk的使用率范围保持在中间水平,降低了PoolChunk被回收的概率,从而兼顾了性能 。
- PoolChunkList
PoolChunkList负责管理多个PoolChunk的生命周期,同一个PoolChunkList中存放的内存使用率相近,这些PoolChunk同样以双链表的形式连接在一起。
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) { // 如果申请的内存大于最大内存,则申请失败 if (normCapacity > maxCapacity) { // Either this PoolChunkList is empty or the requested capacity is larger then the capacity which can // be handled by the PoolChunks that are contained in this PoolChunkList. return false; } for (PoolChunk<T> cur = head; cur != null; cur = cur.next) { if (cur.allocate(buf, reqCapacity, normCapacity)) { // 如果使用率超过 maxUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到下一个 PoolChunkList if (cur.usage() >= maxUsage) { remove(cur); nextList.add(cur); } return true; } } return false; }
每个 PoolChunkList 都有内存使用率的上下限:minUsage 和 maxUsage,当 PoolChunk 进行内存分配后,如果使用率超过 maxUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到下一个 PoolChunkList。同理,PoolChunk 中的内存发生释放后,如果使用率小于 minUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到前一个 PoolChunkList。:minUsage 和 maxUsage,当 PoolChunk 进行内存分配后,如果使用率超过 maxUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到下一个 PoolChunkList。同理,PoolChunk 中的内存发生释放后,如果使用率小于 minUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到前一个 PoolChunkList。
虽然每个 PoolChunkList 都有内存使用率的上下限,但是每个PoolChunkList 的上下限之间都是有交叉重叠的。因为 PoolChunk 需要在 PoolChunkList 不断移动,如果每个 PoolChunkList 的内存使用率的临界值都是恰好衔接的,例如 1 ~ 50%、50% ~ 75%,那么如果 PoolChunk 的使用率一直处于 50% 的临界值,会导致 PoolChunk 在两个 PoolChunkList 不断移动,造成性能损耗。
protected PoolChunk<ByteBuffer> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize) { if (directMemoryCacheAlignment == 0) { return new PoolChunk<ByteBuffer>(this, allocateDirect(chunkSize), pageSize, maxOrder, pageShifts, chunkSize, 0); } final ByteBuffer memory = allocateDirect(chunkSize + directMemoryCacheAlignment); return new PoolChunk<ByteBuffer>(this, memory, pageSize, maxOrder, pageShifts, chunkSize, offsetCacheLine(memory)); }
接下来看Chunk方法的创建过程 。
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) { unpooled = false; this.arena = arena; this.memory = memory; // 存储的数据 this.pageSize = pageSize; // 默认pageSize大小为8K ,也就是8192 this.pageShifts = pageShifts; // 默认为13 this.maxOrder = maxOrder; // 默认为11 this.chunkSize = chunkSize; // chunkSize默认为16M ,也就是2 ^ 24 B this.offset = offset; unusable = (byte) (maxOrder + 1); // log2ChunkSize = log2(chunkSize); // 默认log2ChunkSize为24 subpageOverflowMask = ~(pageSize - 1); // 默认subpageOverflowMask为-8192 freeBytes = chunkSize; // 初始化freeBytes大小为16M assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder; // maxSubpageAllocs 表示1 向右移11位,则等于 2 ^ 11 = 2048 maxSubpageAllocs = 1 << maxOrder; // Generate the memory map. // 在PoolChunk中,用一个数组memoryMap维护了所有节点(节点数 为1~2048×2-1)及其对应的高度值。memoryMap是在PoolChunk初始 化时构建的, // 其下标为图中节点的位置,其值为节点的高度,如 memoryMap[1]=0 、 memoryMap[2]=1 、 memoryMap[2048]=11 、 memoryMap[4091]=11 // 满二叉树的节点是否被分配,数组的大小为4096 // 用户记录二叉树节点的分配信息, memoryMap 初始值与depthMap是一样的, 随着节点被分配,不仅节点值会改变, 而且会递归遍历更新其父节点的值 // 父亲节点的值取两个子节点中最小的值 memoryMap = new byte[maxSubpageAllocs << 1]; // 除memoryMap之外,还有一个同样的数组——depthMap。两 者的区别是:depthMap一直不会改变,通过depthMap可以获取节点的 内存大小,还可以获取节点的 // 初始高度值;而memoryMap的节点和父节 点对应的高度值会随着节点内存的分配发生变化。当节点被全部分配 完时,它的高度值会变成12,表示 // 目前已被占用,不可再被分配,并 且会循环递归地更新其上所有父节点的高度值,高度值都会加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 ++; } } // PoolChunk中管理的2048个8K的内存块, 对应图中的PoolChun的内部的Page0,Page1, Page2 ... Page2047 ,Netty 中并没有Page 的定义,直接使用PoolSubpage表示 subpages = newSubpageArray(maxSubpageAllocs); cachedNioBuffers = new ArrayDeque<ByteBuffer>(8); } private PoolSubpage<T>[] newSubpageArray(int size) { return new PoolSubpage[size]; }
之前我们分析过Chunk,刚好碰到Chunk的构造方法,这里再来重复一遍。
Chunk 中用了一个平衡二叉树来表示它的内存使用状况,树中的一个节点代表某一范围的内存,同一深度的所有节点代表的内存大小是相同的,并且用一个byte数组memoryMap表示所有节点的状态,memoryMap也就是PoolChunk构造方法中的memoryMap属性,节点的状态反映它所代表这段内存的使用情况,节点的状态有三种情况 。
- 如果这段内存全部被使用,节点的状态值为12,最大深度 + 1
- 如果这段内存部分被使用,节点的状态值为他所在的深度 + 1
- 如果这段内存完全被使用,节点状态的值为他所在的深度。
每次申请内存时,都会根据申请的大小从对应的深度中开始查找,例如,如果申请一个4MB 的内存,就会从d = 2 开始查找(因为深度为2的Node都是大小为4M的内存) 。
- 先从Node4开始,如果Node4的状态值为2, 那么表示 0 ~ 4 M这一块内存完全未被使用,就会将 0 ~ 4M 这一块内存返回给申请者,并修改相应的节点的状态: Node4 的状态值为12(表示 0 ~ 4M这一块内存已经全部被使用),Node2 的状态值改为2 (Node2 所在的深度值 + 1 ),Node1 的状态值为1(因为0 ~ 8M 和 0 ~ 16M 只是部分被使用) 。
- 如果Node4的状态值为3或者12,表示 0 ~ 4M 这一块内存已经部分被完成使用了,则继续查看Node5的状态值 。
- 如果Node4的状态值为3或者12,表示0~4M这一块内存已经部分或完全被使用了,则继续查看Node5的状态 。
- 如果d=2的节点状态值都不为2 ,表示Chunk 已经不存在连续的4M 大小的内存了, 则申请失败,继续从ChunkList的下一个Chunk中去申请 。
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) { final long handle; // 内存指针,分配的二叉树内存节点偏移量或page和PoolSubpage的偏移量 if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize 分配大于或等于page 的内存 handle = allocateRun(normCapacity); // 具体分配的内存节点的偏移量 } else { handle = allocateSubpage(normCapacity); // page 和PoolSubpage的偏移量 } if (handle < 0) { // 分配失败 return false; } // 从缓存的ByteBuffer对象池中获取一个ByteBuffer对象,有可能为null ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null; // 初始化申请到的内存数据,并对PoolByteBuf对象进行初始化 initBuf(buf, nioBuffer, handle, reqCapacity); return true; }
每个 PoolChunk 默认大小为 16M,PoolChunk 是通过伙伴算法管理多个 Page,每个 PoolChunk 被划分为 2048 个 Page,最终通过一颗满二叉树实现。伙伴算法尽可能保证了分配内存地址的连续性,可以有效地降低内存碎片。
假如用户需要依次申请 8K、16K、8K 的内存。首先看下分配逻辑 allocateRun 的源码,如下所示。PoolChunk 分配 Page 主要分为三步:首先根据分配内存大小计算二叉树所在节点的高度,然后查找对应高度中是否存在可用节点,如果分配成功则减去已分配的内存大小得到剩余可用空间。
/** * 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; }
查找对应高度中是否存在可用节点的方法很有意思,我们进入这个方法 。
/** * Algorithm to allocate an index in memoryMap when we query for a free node * at depth d * * @param d depth * @return index in memoryMap * Netty源码是如何查找对应的可用节点并更新其父节点的高度值的呢? * Netty采用了前序遍历算法,从根节点开始,第二层为左右节点, 先看左边节点内存是否够分配,若不够,则选择其兄弟节点(右节点); * 若当前左节点够分配,则需要继续向下一层层地查找,直到找 到层级最接近d(分配的内存在二叉树中对应的层级)的节点。具体查 找算法如下: * d 是申请的内存在PoolChunk二叉树中的高度值,若内存为8KB,则 d 为11 */ private int allocateNode(int d) { int id = 1; // d 是申请的内存在PoolChunk 二叉树中的高度值,若内存为8KB,则d为11 int initial = -(1 << d); // has last d bits = 0 and rest all = 1 掩码,与id进行与操作后,若> 0 ,则说明id 对应的高度大于或等于d byte val = value(id); // 为memoryMap[id] if (val > d) { // unusable // 若当前分配的空间无法满足要求 , 则直接返回-1,分配失败 return -1; } // 有空间可以分配了,就需要一步一步的找到更接近高度值的d的节点,若找到的高度值等于d, 但此时其下标与initial进行与操作后的0 ,则说明 // 其子节点有一个未被分配,且其初始化层级 < d ,只是由于其有一个节点被分配了,所以层级val 与 d 相等 while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0 id <<= 1; // 每次都需要把id向树的叶子节点下移动一层, 即左移一位 val = value(id); // 获取id对应的层级高度值值memoryMap[id] if (val > d) { // 若id对应的层级高度值大于d,表示这一块内存已经全部被使用 , 则此时去其兄弟节点找,肯定能找到 id ^= 1; // 获取其兄弟节点,兄弟节点的位置通过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); // 返回id(1~2048*2-1) 通过id 可以获取其层级高度值,也可以算出其占用的内存空间的大小 return id; } 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; } }
上面updateParentsAlloc()方法就是真正的实现下面三个逻辑的地方。
- 如果这段内存全部被使用,节点的状态值为12,最大深度 + 1
- 如果这段内存部分被使用,节点的状态值为他所在的深度 + 1
- 如果这段内存完全被使用,节点状态的值为他所在的深度。
结合 PoolChunk 的二叉树结构以及 allocateRun 源码我们开始分析模拟的示例:
第一次分配 8K 大小的内存时,通过 d = maxOrder - (log2(normCapacity) - pageShifts) 计算得到二叉树所在节点高度为 11,其中 maxOrder 为二叉树的最大高度,normCapacity 为 8K,pageShifts 默认值为 13,因为只有当申请内存大小大于 2^13 = 8K 时才会使用 allocateRun 分配内存。然后从第 11 层查找可用的 Page,下标为 2048 的节点可以被用于分配内存,即 Page[0] 被分配使用,此时赋值 memoryMap[2048] = 12,表示该节点已经不可用,然后递归更新父节点的值,父节点的值取两个子节点的最小值,memoryMap[1024] = 11,memoryMap[512] = 10,以此类推直至 memoryMap[1] = 1,更新后的二叉树分配结果如下图所示。
第二次分配 16K 大小内存时,计算得到所需节点的高度为 10。此时 1024 节点已经分配了一个 8K 内存,不再满足条件,继续寻找到 1025 节点。1025 节点并未使用过,满足分配条件,于是将 1025 节点的两个子节点 2050 和 2051 全部分配出去,并赋值 memoryMap[2050] = 12,memoryMap[2051] = 12,再次递归更新父节点的值,更新后的二叉树分配结果如下图所示。
第三次再次分配 8K 大小的内存时,依然从二叉树第 11 层开始查找,2048 已经被使用,2049 可以被分配,赋值 memoryMap[2049] = 12,并递归更新父节点值,memoryMap[1024] = 12,memoryMap[512] = 12,以此类推直至 memoryMap[1] = 1,最终的二叉树分配结果如下图所示。
当Subpage 级别的内存分配
在分配小于 8K 的内存时,PoolChunk 不在分配单独的 Page,而是将 Page 划分为更小的内存块,由 PoolSubpage 进行管理。首先我们看下 PoolSubpage 的创建过程,由于分配的内存小于 8K,所以走到了 allocateSubpage 源码中:
private long allocateSubpage(int normCapacity) { // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it. // This is need as we may add it back and so alter the linked-list structure. // 通过优化后的内存容量找到Areana的两个subpages缓存池其中 一个对应的空间head指针 // 根据内存大小找到PoolArena中subpage数组对应的头结点 PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity); int d = maxOrder; // subpages are only be allocated from pages i.e., leaves 小于8KB 内存只在11层分配,因为分配内存小于8K ,所以从满二叉树最底层开始查找 // 由于分配前需要把PoolSubpage加入缓存池中,以便一回直接从Arean的缓存池中获取,因此选择加锁head指针 synchronized (head) { int id = allocateNode(d); // 在满二叉树中获取一个可用的节点 if (id < 0) { return id; } final PoolSubpage<T>[] subpages = this.subpages; // 记录哪些Page 转化为Subpage final int pageSize = this.pageSize; freeBytes -= pageSize; // 可用空间减去一个page,表示此page被占用 // pageId 到subpageId的转化,例如,pageId=2048 对应的subpageId = 0 int subpageIdx = subpageIdx(id); // 根据page的偏移值减2048获取PoolSubpage的索引 PoolSubpage<T> subpage = subpages[subpageIdx]; // 获取page对应的PoolSubpage if (subpage == null) { // 若为空,则初始化一个,初始化会运行PoolSubpage的addToPool()方法,把subpage追加到head的后面 // 创建PoolSubpage,并切分为相同大小的子内存块,然后加入PoolArena对应的链表中 subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity); subpages[subpageIdx] = subpage; } else { subpage.init(head, normCapacity); // 初始化同样会调用addToPool()方法,此处思考,什么情况下才会发生这类情况 } return subpage.allocate(); // PoolSubpage 的内存分配 ,执行内存分配并返回内存地址 } }
PoolSubpage<T> findSubpagePoolHead(int elemSize) { int tableIdx; PoolSubpage<T>[] table; // < 512 判断是否 < 512,如果小于512 if (isTiny(elemSize)) { tableIdx = elemSize >>> 4; // 除以16即可 table = tinySubpagePools; } else { tableIdx = 0; elemSize >>>= 10; // 除以1024 while (elemSize != 0) { // elemSize 大于或等于1024 elemSize >>>= 1; // 除以2 ,因此后续字节都以2为倍数来增长的 tableIdx ++; } table = smallSubpagePools; } return table[tableIdx]; }
findSubpagePoolHead()方法其实是从tinySubPagePools或smallSubPagePools数组中找头节点的过程 。
接下来看PoolSubpage构造函数的初始化 。 但在进入方法之前需要记住 Subpage 没有固定的大小,需要根据用户分配的缓冲区大小决定,例如分配 1K 的内存时,Netty 会把一个 Page 等分为 8 个 1K 的 Subpage,如果申请的内存大小为16B,则会等分为512个16B 的Subpage
final class PoolSubpage<T> implements PoolSubpageMetric { final PoolChunk<T> chunk; // 当前分配内存的chunk,表明该subpage属于哪一个Chunk // 当前page在chunk的memoryMap中的下标 id, 表明该subpage在二叉树的节点编号,由于subpage是由Page // 转换而来,而Page都在二叉树的最后一层,因此这个值一定在2048~4095之间 private final int memoryMapIdx; private final int runOffset; // 当page 在chunk的memory上的偏移量 private final int pageSize; // page的大小 ,默认是8192b ,也就是8K private final long[] bitmap; // poolSubpage每段内存的占用状态,采用二进制位来标识 ,用于标记element是否可用 PoolSubpage<T> prev; // 指向前一个PoolSubpage PoolSubpage<T> next; // 指向后一个PoolSubpage boolean doNotDestroy; // Page 转换为subpage后,每个element的大小,在上个例子中, 这个值为32,elemSize决定了这个subpage在tinySubpagePools或 // smallSubpagePools数组中的位置 int elemSize; private int maxNumElems; // 这个subpage有多少个element, 这个段等于8K/elmSize private int bitmapLength; // 实际采用二进制位标识的long数组的长度值,根据每段大小elementSize 和pageSize业计算得来的 private int nextAvail; // 下一个可用的element位置 private int numAvail; // 可用的element的数量 PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) { this.chunk = chunk; this.memoryMapIdx = memoryMapIdx; this.runOffset = runOffset; this.pageSize = pageSize; bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64 等价于 2 ^ 13 / 2 ^ 4 / 2 ^ 6 = 2 ^ 3 = 8 init(head, elemSize); } }
大家肯定知道,每一个pageSize大小是8K,也就是8192B,对于tinySubPagePools的内存规格为16B,32B,48B,64B,80B,96B,112B,128B,144B,160B,176B,192B,208B,224B,240B,256B,272B,288B,304B,320B,336B,352B,368B,384B,400B,416B,432B,448B,464B,480B,496B, 而smallSubPagePools的内存规格为512B,1024B,2048B,4096B ,而bitmap就是用一个Long数组来标识内存块有没有被使用,在tinySubPagePools和smallSubPagePools中,内存规格最小的为16B, 而一个long类型的变量占64位,那么需要多少个long变量来标识16B的内存块有没有被使用呢? 显然就是 8192 / 16 / 64 = 8,因此bitmap的数组长度为8 。 以下图为例子,假如申请的内存大小为16B,那PoolSubpage被拆分为512个16B的内存块,需要8个long类型的变量不标识内存块的使用情况,如果bitmap[0]的第一位和第二位被标识为1,则表示PoolSubpage的第一个和第二个内存块已经被使用。
有人会想,如果现在申请的内存大小为1024B,那么bitmap的长度还是8吗? 答案依然是8 ,只不过只有bitmap[0]的低8位用来记录PoolSubpage内存块的使用情况,bitmap的其他位置将不作使用。
有了上面这些基础知识,再来看init()方法将方便很多。
void init(PoolSubpage<T> head, int elemSize) { doNotDestroy = true; this.elemSize = elemSize; // 每个element的大小 if (elemSize != 0) { maxNumElems = numAvail = pageSize / elemSize; // numAvail, 可用element的数量,maxNumElems 最大element数量 nextAvail = 0; // 初始化时,下一个可用element的位置 // maxNumElems >>> 6 等价于 最大element数量 / 64,每一个long类型有64位 // bitmapLength 表示需要多少个long值来标识element是否被使用 bitmapLength = maxNumElems >>> 6; // 63对应的二进制数为 0000 0000 0000 0000 0000 0000 0011 1111 if ((maxNumElems & 63) != 0) { // 如果 maxNumElems & 63 != 0 bitmapLength ++; } for (int i = 0; i < bitmapLength; i ++) { bitmap[i] = 0; } } addToPool(head); }
由于PoolSubpage每段的最小值为16B,因此它的段的总数量最多 为pageSize/16。把PoolSubpage中每段的内存使用情况用一个long[] 数组来标识,long类型的存储位数最大为64 ,每一位用0表示为空闲 状态,用1表示被占用,这个数组的长度为pageSize/16/64 ,默认情 况下,long数组的长度最大为8192/16/64 =8 。每次在分配内存时, 只需查找可分配二进制的位置,即可找到内存在page中的相对偏移量。
private void addToPool(PoolSubpage<T> head) { assert prev == null && next == null; prev = head; // this.prev = head next = head.next; // this.next = head.next next.prev = this; head.next = this; }
关于init()方法中的变量已经注释中已经解释得很清楚了,这里分析一下addToPool()方法,发现PoolSubpage插入方法是不是经典的头插法,这样做有什么益处呢? 请看PoolArena方法的allocate()方法中的if (s != head) { 这一行代码 。
如果发现头节点的next节点不是自身,则直接使用head的next节点进行内存分配即可,如果PoolSubpage使用尾插法,则需要遍历整个链表,一定程度上浪费时间,我们之前分析HashMap源码时,在JDK7中HashMap链表插入方式就使用头插法,可能会导致循环链表的产生,但这里不需要担心,使用了synchronized来解决并发问题。
接下来看subpage的allocate()方法 。
long allocate() { if (elemSize == 0) { return toHandle(0); } if (numAvail == 0 || !doNotDestroy) { return -1; } final int bitmapIdx = getNextAvail();// 获取PoolSubpage下一个可用的位置 int q = bitmapIdx >>> 6; // 获取该位置的bitmap数组对应的下标值,右移6位 2 ^ 6 = 64 int r = bitmapIdx & 63; // 获取bitmap[q]上实际可用的位,63的二进制表示为:0011 1111 assert (bitmap[q] >>> r & 1) == 0; // 确定该位没有被占用 bitmap[q] |= 1L << r; // 将该位设为1,表示已经被占用,此处 1L << r 表示将r 设置为1 if (-- numAvail == 0) { // 若没有可用的段,则说明此page/PoolSubpage已经分配满了, 没有必要到PoolArena池,应该从Pool 中移除 removeFromPool(); } return toHandle(bitmapIdx); // 把当前page的索引和PoolSubPage 的索引一起返回低32位表示page的index,高32位表示PoolSubPage的index } private int getNextAvail() { // 查找下一个可用的位置 int nextAvail = this.nextAvail; // 若下一个可用的位置大于或等于0 , 则说明是每一次分配或正好已经有内存回收,可直接返回 if (nextAvail >= 0) { this.nextAvail = -1; // 每次分配完内存后,都要将nextAvail 设置为-1 return nextAvail; } // 没有直接可用内存,需要继续查找 return findNextAvail(); } private int findNextAvail() { final long[] bitmap = this.bitmap; final int bitmapLength = this.bitmapLength; // 遍历用来标识内存是否被占用的数组 for (int i = 0; i < bitmapLength; i ++) { long bits = bitmap[i]; // 若当前long型的标识位不全为1,则表示其中有未被使用的内存 if (~bits != 0) { return findNextAvail0(i, bits); } } return -1; } private void removeFromPool() { assert prev != null && next != null; prev.next = next; next.prev = prev; next = null; prev = null; } private int findNextAvail0(int i, long bits) { final int maxNumElems = this.maxNumElems; // i 表示 bitMap的位置,由于bitmap 每个值有64位 , 因此用i * 64 来表示bitmap[i]的PoolSubpage中的偏移量 final int baseVal = i << 6; for (int j = 0; j < 64; j ++) { if ((bits & 1) == 0) { // 判断第一位是否为0,为0表示该位空闲 int val = baseVal | j; if (val < maxNumElems) { return val; } else { break; } } bits >>>= 1; // 若bits的第一位不为0 , 则继续右移一位, 判断第二位 } return -1; // 如果没有找到,则返回-1 }
其实findNextAvail()系列方法还是很有意思的,其实就是找bitmap数组中,long 变量中被1占用的位。
如上图所示,如果PoolSubpage的内存块大小为16B, 而bitmap[0]的64位已经被占满,而bitmap[1]的第一位已经被占用,则说明PoolSubpage的前65个内存块已经被分配了,下次分配从PoolSubpage的第66块内存开始分配,而findNextAvail0()方法其实就是计算bitmap[] long类型的数组中被1占用位数的个数。
接下来看另外一个有意思的方法toHandle(), 这个方法实际上返回的是一个long类型的变量,变量的高32位用于存储分配的内存在PoolSubpage的哪个内存块的位置,低32位用于存储PoolSubpage在PoolChunk的位置 。
// 反当前page的索引和PoolSubpage的索引一起返回 , 低32位表示page的index ,高32位表示Poolsubpage的index private long toHandle(int bitmapIdx) { return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx; }
toHandle()方法高32位用来存储申请的内存在PoolSubpage的位置
低32位用来存储当前PoolSubpage在PoolChunk数组中的位置。
低32位的0010转化为10进制为2 ,表示当前PoolSubpage在PoolChunk数组中的第二个位置 , 高32 二进制为 0011 转化为10进制为3 ,表示申请的内存在PoolSubpage 第三块内存。
接下来进入initBuf()方法 。
void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) { int memoryMapIdx = memoryMapIdx(handle); // 用int强转, 取handle的低32位, 低32位存储的是二叉树节点的位置 int bitmapIdx = bitmapIdx(handle); // 右移32位并强制转为int 型 ,相当于获取handle的高32位, 即PoolSubpage的内存段相对page的偏移量 if (bitmapIdx == 0) { // 无PoolSubpage,即大于或等于page的内存分配 byte val = value(memoryMapIdx); // 获取节点的高度 assert val == unusable : String.valueOf(val); // 判断节点的高度是不可用的(默认为12), buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset, reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache()); // 计算偏移量,offset的值为内存对齐偏移量 } else { initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity); } } private static int bitmapIdx(long handle) { return (int) (handle >>> Integer.SIZE); }
大家可能对bitmapIdx == 0 表示的是分配内存大于8K,比较困惑,为什么呢? 我们看之前的allocate()方法 。
当分配的内存大小大于8192B时,会调用allocateRun()方法,而allocateRun()方法返回的是节点id,并不是被toHandle()方法处理过的值,因此在initBuf()方法中也是分两种情况,如果分配的内存大于8192B和分配的内存小于8192B。 无论分配的内存大于8192B还是小于8192B,都会调用buf的init()方法 。
private int runOffset(int id) { // represents the 0-based offset in #bytes from start of the byte-array chunk int shift = id ^ 1 << depth(id); return shift * runLength(id); } // 深度,比如 2049 的深度为11 private byte depth(int id) { return depthMap[id]; } // 假如id为2049 ,则深度为11,那他的runLength()的大小为8k // 假如id = 1025, 那深度为10,runLength()方法计算得到的值为16K private int runLength(int id) { // represents the size in #bytes supported by node 'id' in the tree return 1 << log2ChunkSize - depth(id); }
同样还是以上图为例子,计算runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize 的值,假设memoryMapIdx为2049 , bitmapIdx为2, subpage.elemSize 为16B,memoryMapIdx相对于2048的偏移量是1,而一个PoolSubpage的大小为8192B,因此runOffset(memoryMapIdx)为8192B * 1 = 8192 ,从而runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize 的值为8192 + 32 = 8224。
void init(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int offset, int length, int maxLength, PoolThreadCache cache) { init0(chunk, nioBuffer, handle, offset, length, maxLength, cache); } private void init0(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int offset, int length, int maxLength, PoolThreadCache cache) { assert handle >= 0; assert chunk != null; // 大内存块默认为16MB, 被分配给多个PooledByteBuf this.chunk = chunk; // chunk中具体的缓存空间 memory = chunk.memory; // 将 PooledByteBuf 转换成ByteBuffer tmpNioBuf = nioBuffer; // 内存分配器:PooledByteBuf是由 Arena 的分配器构建的 allocator = chunk.arena.parent; // 线程缓存,优先从线程缓存中获取 this.cache = cache; // 通过这个指针可以得到PooledByteBuf 在chunk这棵二叉树中具体位置 this.handle = handle; // 偏移量 this.offset = offset; // 长度 ,实际数据长度 this.length = length; // 写指针不能超过PooledByteBuf的最大可用长度 this.maxLength = maxLength; }
Netty内存回收原理
当用户线程内存时会将内存块缓存到本地线程的私有缓存PoolThreadCache中,这样在下次分配内存时会提高分配效率,但是当内存块被用完一次后,再没有分配需求,那么一直驻留在内存中又会造成浪费,接下来看下Netty 是如何实现内存释放的呢? 直接跟进下PoolThreadCache的源码 。
void trim() { trim(tinySubPageDirectCaches); trim(smallSubPageDirectCaches); trim(normalDirectCaches); trim(tinySubPageHeapCaches); trim(smallSubPageHeapCaches); trim(normalHeapCaches); }
从源码中可以看出 , Netty记录了allocate()的执行次数,默认执行8192次,就会触发PoolThreadCache调用一次trim()进行内存整理,会对PoolThreadCache中维护了六个MemoryRegionCache数组分别进行整理,继续跟进trim()的源码,定位的核心逻辑 。
private static void trim(MemoryRegionCache<?>[] caches) { if (caches == null) { return; } for (MemoryRegionCache<?> c: caches) { trim(c); } } private static void trim(MemoryRegionCache<?> cache) { if (cache == null) { return; } cache.trim(); } public final void trim() { // 通过size - allocations 衡量内存分配执行的频繁程度, // 其中size为该MemoryRegionCache对应的内存规格大小,size为固定值,例如 Tiny类型默认为512 // allocations 表示MemoryRegionCache距离上一次内存整理已经发生了多少次allocate调用,当调用的次数小于size时,表示MemoryRegionCache中缓存的内存 // 并不常用,从队列中取出内存块依次释放。 int free = size - allocations; allocations = 0; // We not even allocated all the number that are if (free > 0) { free(free, false); } }
通过size - allocations 衡量内存分配执行的频繁程度,其中size为该MemoryRegionCache对应的内存规格大小,size为固定值,例如Tiny类型的默认值为512,allocations 表示MemoryRegionCache距离上一次内存整理已经发生了多少次allocate调用,当调用的次数小于size时,表示MemoryRegionCache中缓存的内存块并不常用,从队列中取出内存块依次释放 。
private int free(int max, boolean finalizer) { int numFreed = 0; for (; numFreed < max; numFreed++) { Entry<T> entry = queue.poll(); if (entry != null) { freeEntry(entry, finalizer); } else { // all cleared return numFreed; } } return numFreed; }
之前提到过,Tiny的大小size默认大小为512,而在上面的方法中,max的值为size - allocations,而max的大小决定了从队列中poll()的次数,为什么这么设计呢? 首先弄清楚Tiny的大小size默认大小为512的由来,我们知道Tiny的内存规格范围为16B ~ 496B,而一个PoolSubpage的大小为8K,因此一个PoolSubpage假如以16B为单位分配内存块, 那么最多能分割成8192/16 = 512个内存块。 因此这里设计size的值为512,也就是说,无论PoolSubpage怎样分割内存块,他的内存块最多也只有512个,为什么max 的值为 size - allocations呢? 假如PoolSubpage 有512个内存块,此时分配了200次,最多还有512 - 200 = 312个内存块可被回收,因此最多只需要poll() 312次,当然,实际上PoolSubpage 切割的内存块并不是512个,也可能远远小于512,实际情况可能多 从队列中poll()几次 。
private void freeEntry(Entry entry, boolean finalizer) { PoolChunk chunk = entry.chunk; long handle = entry.handle; ByteBuffer nioBuffer = entry.nioBuffer; // 如果不是线程销毁,则finalizer为true // 如果是netty自己调用free()方法,则finalizer为false // 而recycle()的实际作用就是将chunk ,nioBuffer 设置为空 , 有助于jvm回收 // handle 设置为-1 if (!finalizer) { // recycle now so PoolChunk can be GC'ed. This will only be done if this is not freed because of // a finalizer. entry.recycle(); } chunk.arena.freeChunk(chunk, handle, sizeClass, nioBuffer, finalizer); } void recycle() { chunk = null; nioBuffer = null; handle = -1; recyclerHandle.recycle(this); }
void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) { final boolean destroyChunk; synchronized (this) { // We only call this if freeChunk is not called because of the PoolThreadCache finalizer as otherwise this // may fail due lazy class-loading in for example tomcat. if (!finalizer) { switch (sizeClass) { case Normal: ++deallocationsNormal; break; case Small: ++deallocationsSmall; break; case Tiny: ++deallocationsTiny; break; default: throw new Error(); } } destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer); } if (destroyChunk) { // destroyChunk not need to be called while holding the synchronized lock. destroyChunk(chunk); } }
接着继续看调用父类的PoolChunkList的free()方法 。
// PoolChunkList的free()方法 boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) { // 先调用chunk的free()方法,把内存标记为已经释放 chunk.free(handle, nioBuffer); if (chunk.usage() < minUsage) { // 若内存利用率小于minUsage , 则从此需要把PoolChunk 从当前PoolChunkList 中移除 remove(chunk); // Move the PoolChunk down the PoolChunkList linked-list. // 把移除的PoolChunk移到到前一个PoolChunkList中 return move0(chunk); } return true; } // 从PoolChunk链表中移除 private void remove(PoolChunk<T> cur) { if (cur == head) { // 若当前chunk为PoolChunkList的第一个 head = cur.next; // 把chunk的下一个元素变成 PoolChunkList的第一个元素 if (head != null) { head.prev = null; } } else { // 修改指针的指向 PoolChunk<T> next = cur.next; cur.prev.next = next; // 把chunk前面的chunk的next指针指向当前chunk的next if (next != null) { // 若当前chunk的下一个chunk不为空 next.prev = cur.prev; // 则把当前chunk的下下个chunk的prev指针从当前chunk改成当前chunk的前一个元素 } } }
上面的注释已经写得很清楚了,接下来看把移除的PoolChunk移到到前一个PoolChunkList中。
private boolean move0(PoolChunk<T> chunk) { // 在当前PoolChunkList为q000时,直接物理释放 if (prevList == null) { // There is no previous PoolChunkList so return false which result in having the PoolChunk destroyed and // all memory associated with the PoolChunk will be released. assert chunk.usage() == 0; return false; } // 把PoolChunk移到到前面的PoolChunkList中 return prevList.move(chunk); }
接下来看前一个prevList的remove()方法 。
private boolean move(PoolChunk<T> chunk) { assert chunk.usage() < maxUsage; if (chunk.usage() < minUsage) { // Move the PoolChunk down the PoolChunkList linked-list. // 如果chunk的使用率依然小于prevList的使用率,则需要继续向前移动 return move0(chunk); } // PoolChunk fits into this PoolChunkList, adding it here. // 此时chunk的使用率一定是大于minUsage 并且 小于 maxUsage // 则将chunk加入到prevList 列表中 add0(chunk); return true; } void add0(PoolChunk<T> chunk) { chunk.parent = this; // 如果preList的head为空,则直接将当前chunk作为头节点 if (head == null) { head = chunk; chunk.prev = null; chunk.next = null; } else { // 如果preList的head不为空 ,则将当前chunk插入到头节点 chunk.prev = null; chunk.next = head; head.prev = chunk; head = chunk; } }
每个 PoolChunkList 都有内存使用率的上下限:minUsage 和 maxUsage,当 PoolChunk 进行内存分配后,如果PoolChunk的使用率超过PoolChunkList设置的maxUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到下一个 PoolChunkList。同理,PoolChunk 中的内存发生释放后,如果使用率PoolChunk小于PoolChunkList设置的 minUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到前一个 PoolChunkList。
虽然每个 PoolChunkList 都有内存使用率的上下限,但是每个PoolChunkList 的上下限之间都是有交叉重叠的。因为 PoolChunk 需要在 PoolChunkList 不断移动,如果每个 PoolChunkList 的内存使用率的临界值都是恰好衔接的,例如 1 ~ 50%、50% ~ 75%,那么如果 PoolChunk 的使用率一直处于 50% 的临界值,会导致 PoolChunk 在两个 PoolChunkList 不断移动,造成性能损耗。
接下来看chunk的free()方法 。
void free(long handle, ByteBuffer nioBuffer) { int memoryMapIdx = memoryMapIdx(handle); int bitmapIdx = bitmapIdx(handle); if (bitmapIdx != 0) { // free a subpage // 先释放subpage PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)]; assert subpage != null && subpage.doNotDestroy; // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it. // This is need as we may add it back and so alter the linked-list structure // 与分配时一样, 先去Area池中找到subpage对应的head指针 PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize); synchronized (head) { // 获取32位bitmapIdx次给PoolSubpage释放,释放后返回true,不再继续释放 if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) { return; } } } freeBytes += runLength(memoryMapIdx); // 释放的字节数调整 setValue(memoryMapIdx, depth(memoryMapIdx)); // 设置节点值为节点初始化值,depth()方法使用的是byte[] depthMap,此字节初始化后就不再改变了 updateParentsFree(memoryMapIdx); // 更新父亲节点的高度值 if (nioBuffer != null && cachedNioBuffers != null && // 把nioBuffer放入到缓存队列中,以便下次再直接使用 cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) { cachedNioBuffers.offer(nioBuffer); } }
boolean free(PoolSubpage<T> head, int bitmapIdx) { if (elemSize == 0) { return true; } int q = bitmapIdx >>> 6; // 由于long型 是64位,因此除了64 就是long[] bitmap的下标 int r = bitmapIdx & 63; // 找到bitmap[q]对应的位置 assert (bitmap[q] >>> r & 1) != 0; // 判断当前位是否为已经分配状态 bitmap[q] ^= 1L << r; // 把bitmap[q] 的 r 位设置为0,表示未分配 setNextAvail(bitmapIdx); // 将该位置设置为下一个可用的位置,这也是在分配时会发生nextAvail大于0的情况 // 若之前没有可分配的内存,从池中移除了, 则将PoolSubpage继续添加到Arena的缓存池中,以便下回分配 if (numAvail ++ == 0) { addToPool(head); return true; } // 若还有没被释放的内存,则直接返回 if (numAvail != maxNumElems) { return true; } else { // Subpage not in use (numAvail == maxNumElems) 若内存全部被释放了,且池中没有其他的PoolSubpage,则不从池中移除,直接返回 if (prev == next) { // Do not remove if this subpage is the only one left in the pool. return true; } // Remove this subpage from the pool if there are other subpages left in the pool. doNotDestroy = false; // 若池中还有其他的节点,且当前节点内存已经全部被释放,则从池中移除,并返回false , 对其上的page也会进行相应的回收 removeFromPool(); return false; } } private void addToPool(PoolSubpage<T> head) { assert prev == null && next == null; prev = head; // this.prev = head next = head.next; // this.next = head.next next.prev = this; head.next = this; }
内存的释放相对其分配来说要简单很多,下面主要剖析PoolChunk 和PoolSubpage的释放。当内存释放时,
同样先根据handle指针找到内存在PoolChunk和PoolSubpage中的相对偏移量,具体释放步骤如下。
-
若在PoolSubpage上的偏移量大于0,则交给PoolSubpage去释放,这与PoolSubpage内存申请有些相似,根据PoolSubpage内存分配段的偏移位
bitmapIdx找到long[]数组bitmap的索引q,将bitmap[q] 的具体内存占用位r置为0(表示释放)。同时调整Arena中的 PoolSubpage缓存池,
若PoolSubpage已全部释放了,且池中除了它还有其他节点,则从池中移除;若由于之前PoolSubpage的内存段全部分配完并从池中移除,
则在其当前可用内存段numAvail等于-1且 PoolSubpage释放后,对可用内存段进行“++”运算,从而使 numAvail++等于0,此时会把释放
的PoolSubpage追加到Arena的 PoolSubpage缓存池中,方便下次直接从缓冲池中获取。 -
若在PoolSubpage上的偏移量等于0,或者PoolSubpage释放完后返回false(PoolSubpage已全部释放完,同时从Arena的 PoolSubpage缓存池中移除了),
则只需更新PoolChunk二叉树对应节点的高度值,并更新其所有父节点的高度值及可用字节数即可。
如果想像力不够,可以结合下面的图进行分析 。
接下来看PoolArena的内存释放
// PoolArena除了内存分配,还管理内存的释放,内存释放代码如下 void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) { // 非内存池内存释放比较简单,直接物理释放即可 if (chunk.unpooled) { int size = chunk.chunkSize(); destroyChunk(chunk); activeBytesHuge.add(-size); deallocationsHuge.increment(); } else { SizeClass sizeClass = sizeClass(normCapacity); // 先尝试放入线程本地缓存,在线程本地缓存默认的情况下,缓存tiny类型的PoolSubpage数最多为64个, if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) { // cached so not free it. return; } freeChunk(chunk, handle, sizeClass, nioBuffer, false); } }
如果在本地缓存,则先尝试放入到本地缓存中,接下来看放入本地缓存的代码实现。
boolean add(PoolArena<?> area, PoolChunk chunk, ByteBuffer nioBuffer, long handle, int normCapacity, SizeClass sizeClass) { // 根据内存规格,获取tinySubPageHeapCaches,smallSubPageHeapCaches, tinySubPageDirectCaches , // smallSubPageDirectCaches, normalHeapCaches, normalDirectCaches 的头节点 MemoryRegionCache<?> cache = cache(area, normCapacity, sizeClass); if (cache == null) { return false; } return cache.add(chunk, nioBuffer, handle); } private MemoryRegionCache<?> cache(PoolArena<?> area, int normCapacity, SizeClass sizeClass) { switch (sizeClass) { case Normal: return cacheForNormal(area, normCapacity); case Small: return cacheForSmall(area, normCapacity); case Tiny: return cacheForTiny(area, normCapacity); default: throw new Error(); } } public final boolean add(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle) { Entry<T> entry = newEntry(chunk, nioBuffer, handle); boolean queued = queue.offer(entry); if (!queued) { // If it was not possible to cache the chunk, immediately recycle the entry entry.recycle(); } return queued; }
看到没有 , Netty 并没有将缓存归还给 PoolChunk,而是使用 PoolThreadCache 缓存起来,当下次有同样规格的内存分配时,直接从 PoolThreadCache 取出使用即可。
我们发现PoolArena中有tinySubpagePools,和smallSubpagePools,
而PoolThreadCache有 tinySubPageDirectCaches,smallSubPageDirectCaches,normalDirectCaches
他们的名字很像,但有什么区别呢?
tinySubpagePools和smallSubpagePools的结构与上面讲解PoolThreadCache中所提到的tinySubPageCaches,smallSubPageCaches的结构类似,所不同的是数组的类型一个是PoolSubpage,一个是MemoryRegionCache,且tinySubpagePools和smallSubpagePools中相同大小的PoolSubpage是以双向链表的形式连接在一起,而不是队列,每个双向链表的head节点都是一个虚拟节点,如图所示:
弄明白了这些,再来看在PoolArena的free()方法中,有一行if (chunk.unpooled) {… }这样的代码,如果chunk.unpooled为false,则不会加入到PoolThreadCache中,那什么情况下chunk.unpooled会为false呢?请看之前分配内存的代码 。
从上图中可以看出,只有在申请的内存大于16M时,创建的PoolChunk的chunk.unpooled才会为true 。
当然啦,线程销毁时PoolThreadCache会依次释放所有的MemoryRegionCache中的内存数据,其中free方法的核心逻辑与之前内存整理trim中释放的内存的过程是一致的。
protected void finalize() throws Throwable { try { super.finalize(); } finally { // 此外Netty在线程退出的时候还会回收该线程的所有内存,PoolThreadCache重载了finalize()方法,在销毁前执行缓存回收的逻辑,对应源码如下: free(true); } } void free(boolean finalizer) { // As free() may be called either by the finalizer or by FastThreadLocal.onRemoval(...) we need to ensure // we only call this one time. // 在线程销毁时,PoolThreadCache会依次释放所有的MemoryRegion中的内存数据,其中free方法的核心逻辑与之前内存整理的trim中的释放内存的逻辑是一致的。 if (freed.compareAndSet(false, true)) { int numFreed = free(tinySubPageDirectCaches, finalizer) + free(smallSubPageDirectCaches, finalizer) + free(normalDirectCaches, finalizer) + free(tinySubPageHeapCaches, finalizer) + free(smallSubPageHeapCaches, finalizer) + free(normalHeapCaches, finalizer); if (numFreed > 0 && logger.isDebugEnabled()) { logger.debug("Freed {} thread-local buffer(s) from thread: {}", numFreed, Thread.currentThread().getName()); } if (directArena != null) { directArena.numThreadCaches.getAndDecrement(); } if (heapArena != null) { heapArena.numThreadCaches.getAndDecrement(); } } }
内存的分配和释放基本已经分析完成,但大家有没有一种感觉,去看单个分配和释放的逻辑时还是比较简单,但整个分析和释放的过程总觉得还是比较模糊,因此从下图中展示PoolArena,PoolChunk, PoolSubpage, tinySubpagePools,smallSubpagePools, PoolThreadCache, tinySubPageDirectCaches,smallSubPageDirectCaches,normalDirectCaches他们之间的关系 。
我相信,如果不是深入源码的话,看上图还是比较晕的,上图中总共画了7条线,我们分析每条线之间的关系 。
-
第一条线,我们知道,PoolArena中管理着内存分配,PoolArena中有6种类型的PoolChunkList,分别为qInit,q000,q025,q050,q075,q100。 他们之间以双链表的形式链接。PoolChunk根据自身的内存使用率在PoolChunkList链表中移动。每一个PoolChunk的内存大小为16M,因此最多可以分成2048个8K的内存块。当然也可以分配1024个16K的内存块,当然啦,PoolChunk是通过memoryMap来标识这一块内存有没有被使用,就是上图中所示的二叉树,
如果这段内存全部被使用,节点的状态值为12(最大深度+1)
如果这段内存部分被使用,节点的状态值为他所在的深度+1
如果这段内存完全未被使用,节点的状态值为他所在的深度。 -
第二条线, 如果我们申请一个12B的内存,Netty会规整为16B,此时如果PoolThreadCache中并将没有内存规格为 16B,因此需要从PoolArena中创建一个Chunk,再从Chunk中的叶子节点选一个8K的Page,再将他转变为PoolSubpage,因此就创建了一个内存规格为16B 且有512个内存块的PoolSubpage,而这个PoolSubpage先添加到PoolArena的tinySubpagePools链表中,当然,如果我们申请的内存为1000B,此时内存会被规整为1024B,刚好是1K,此时Netty 会创建一个内存规格为1K 且内存块为8的PoolSubpage,同样来存放刚刚申请的1000B 的内存。
-
第三条线和第四条线的意思就是,根据用户申请的内存规格,PoolSubpage可以划分为16B,32B,48B,64B,80B,96B,112B,128B,144B,160B,176B,192B,208B,224B,240B,256B,272B,288B,304B,320B,336B,352B,368B,384B,400B,416B,432B,448B,464B,480B,496B,512B,1K,2K,4K 的内存规格。当然不同的内存规格,内存块的个数不一样,如内存规格为16B的PoolSubpage有512个这样的内存块,如内存规格为32B的PoolSubpage有256个这样的内存块。
-
接下来看第5条线的意思,当PoolArena在调用free方法时,如果内存大小在[16B,496B]之间时,此时会调用cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass) 方法将Chunk下的PoolSubpage的某个内存块封装成Entry 添加到PoolThreadCache的tinySubPageDirectCaches队列中,当然,如果内存块的大小在[512B,4k]之间时,同样会通过调用cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass) 方法将内存块封装成Entry添加到smallSubPageDirectCaches队列中,当然如果内存块的大小在[8k,32k]时,还是以相同的方式添加到PoolThreadCache的normalDirectCaches队列中,而Entry有两个参数,第一个参数chunk指向内存块所属的PoolChunk,第二个参数handle,表示内存块在PoolChunk的坐标,高32位表示在PoolSubpage中的位置 ,低32位表示PoolSubpage在PoolChunk的位置。当然如果内存大小在[8K~32K]之间时,此时handle则指向PoolChunk中Node内存块的位置 。如Node2,Node4等。
接下来,来模拟一个16B内存的分配,释放,再分配的过程 。
-
用户申请了16B的内存,此时会调用directArena.allocate(cache, initialCapacity, maxCapacity);方法进行内存分配,在PoolArea的allocate方法中,因为申请的内存小于8k,因此 isTinyOrSmall(normCapacity)方法返回true,此时尝试PoolThreadCache内存分配,调用 cache.allocateTiny(this, buf, reqCapacity, normCapacity) ,但遗憾的是,PoolThreadCache的队列中并没有可分配的内存,因此会尝试从PoolArea的tinySubpagePools 链表中获取可分析内存的PoolSubpage,但遗憾的是,因为第一次请求分配内存, tinySubpagePools中依然没有可分配内存的PoolSubpage,因此只能调用allocateNormal()方法进行内存分配
当然在allocateNormal()方法中,尝试的顺序分别为q050,q025,q000,qInit,q075,当然是第一次分配,在这些PoolChunkList中显然没有可分配的内存,因此会调用PoolChunk c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);方法创建一个Chunk ,并调用他的c.allocate(buf, reqCapacity, normCapacity); 方法进行内存分配,同时将新创建的Chunk添加到qInit中。
接下来看Chunk的allocate()方法实现逻辑。
在allocate()方法中,因为申请的是16B的内存,显然小于8K ,因此会调用allocateSubpage(normCapacity); 进行内存分配 。 当然分配好了,会调用 initBuf(buf, nioBuffer, handle, reqCapacity);方法初始化申请到的内存,这里进入allocateSubpage()方法
在page方法中,其实也很简单,只做了三件事情,第一件事情,找到elemSize内存规格对应的subpagePools的头节点,第二步,如果头节点中没有PoolSubpage,则创建一个新的PoolSubpage,第三步,调用PoolSubpage的allocate()方法分配内存 。 在allocate()方法中分配内存就很简单了,请看下图。
allocate()方法的主要目的就是从PoolSubpage中找到一个可用的内存块,当然,PoolSubpage中内存块可不可用是通过bitmap的long数组中每个元素的位来标识内存块可不可用。 例如 PoolSubpage的内存规格是16B,那么可以划分为512个内存块,而每个long类型有64位,那么需要8个long类型的元素来标识,而每一位的0 表示对应的内存块没有被占用,1 表示对应的内存块已经被占用。因此从bitmap数组中即可找出位为0对应的内存块,而这个内存块就可以用来分配内存,当然找到后,需要将新找到的内存块对应的位设置为1 , 同时用一个toHandle变量来记录该内存块的位置 ,高32位记录内存块在 PoolSubpage的位置 , 低32位记录 PoolSubpage 在PoolChunk中的位置,这里就大概分析了一个16B内存块请求分配的过程 。 -
当然,接下来分析,如果这16B的内存块被释放,又会经历哪些过程呢? 当然,内存释放有三种情况,第一种情况,当然就是当内存分配达到一定的次数,比如达到了8192次后,就会触发一次内存释放。
第二种情况,当线程被销毁时,会调用PoolThreadCache的finalize()方法,此时依然会触发内存的回收。
当然,我们分析第三种情况,如果PooledByteBuf的deallocate()方法进行内存回收。 此时会触发chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache) 内存释放。
如果是16B的内存需要被回收,显然他不是大内存回收,在free()方法中会调用if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) 将当前内存块添加到PoolThreadCache中。
而在cache.add(chunk, nioBuffer, handle)方法中会将Chunk和当前内存块的位置封装成Entry中。
当然啦,聪明的读者肯定会发现,即使这个内存块被添加到queue队列中,但他对应的PoolSubpage的bitmap数组中并没有将这个内存块对应的标志位设置为0。 当然还有另外一种情况,就是添加队列失败,此时将调用一般的内存回收方法freeChunk()。
freeChunk()方法的内部逻辑也很简单。 最终调用了chunk.free(handle, nioBuffer) 方法 。
而chunk.free()方法的逻辑就很简单了,大部分情况最终还是调用了subpage的free(head, bitmapIdx & 0x3FFFFFFF)方法 。
而subpage的free()方法也很简单,无非就是将该内存块在bitmap数组中的标志位设置为0,在这种情况,大家一定要小心,如果这个内存块对应的PoolSubpage在tinySubpagePools或smallSubpagePools链表中,而如果刚好又在其他线程申请16B的内存块,刚刚回收的内存块是可以被再次分配的。
- 对于刚刚被回收的内存块有两种情况,第一种就是被添加到PoolThreadCache的MemoryRegionCache队列中,但他在PoolSubpage的bitmap标志位依然是1,其他线程没有资格去分配这块内存块,还有一种情况,该内存块在PoolSubpage的bitmap中的标志位被设置为0,此时任何线程申请分配内存,都有可能申请到这一块内存,如果刚刚回收的内存块的引用被放到了PoolThreadCache的MemoryRegionCache队列中,此时PoolThreadCache对应线程刚好又申请了相同规格的内存,刚刚回收的内存块是可能被分配的,大家想想,为什么Netty要用一个PoolThreadCache结构来分配内存呢? 请看下面代码。
如果申请的内存不是通过当前线程的PoolThreadCache来分配,则只能到PoolArena的tinySubpagePools和smallSubpagePools申请内存,虽然Netty的处理和ConcurrentHashMap 处理并发问题很像,因为当前申请的内存如果是16B,则只会对tinySubpagePools中16B的链表头节点加锁,而不会对整个tinySubpagePools的PoolSubpage数组中所有的头节点加锁,因此性能上还是不错的, 但如果多个线程同时申请16B的内存,此时整个16B 规格的PoolSubpage链表将被加锁,此时线程之间只能阻塞等待,这也是Netty为什么要用PoolThreadCache 分配内存的原因,如果将PoolArena回收的内存块加到了PoolThreadCache中队列中,毋庸置疑,可能是存在内存浪费的,为什么呢? 如果将回收的内存块添加到PoolThreadCache队列中,但此时拥有PoolThreadCache的线程并没有去申请分配内存,而其他线程刚好需要这么一块内存块,如果此时在PoolArena中并没有可容纳其他线程申请的内存块,那只能重新申请一个PoolChunk来进行内存分配,当然这种情况也是极端的情况,但从侧面看出了,Netty使用这种方式来分配内存,可能存在一定的内存浪费,为什么说可能呢? 因为刚刚举例的这种情况就是属于内存浪费的情况,但还有另外一种情况,如果其他线程来申请内存,此时已经开辟的PoolChunk中有可容纳其他线程申请的内存时,此时就不存在内存浪费的情况,如果一个内存块放到PoolThreadCache 的队列中没有被使用和放在PoolArena的tinySubpagePools中没有被使用原理一样,都属于内存没有被使用的情况,但将回收的内存放到PoolThreadCache的队列中,对加快内存的分配肯定是大有帮助的,因此从一定程度上来讲,Netty也是使用了空间换时间的方式来加快内存的回收。
关于Netty内存分配和回收的分配也告一段落了,我们知道了Netty内存分配和回收的原理,在Netty中,又是如何使用的呢? 在应用Netty时,通过默认设 置PooledByteBufAllocator执行ByteBuf的分配。当用NioByteUnsafe 的 read() 方 法 读 取 NioSocketChannel 数 据 时 , 需 要 调 用 PooledByteBufAllocator去分配内存,具体分配多少内存,由Handle 的guess()方法决定,此方法只预测所需的缓冲区的大小,不进行实际 的 分 配 。 PooledByteBufAllocator 从 PoolThreadLocalCache 中 获 取 PoolArena,最终的内存分配工作由PoolArena完成。
接下来看内存的分配与计算
RecvByteBufAllocator 内存分配与计算
虽然了解了Netty整个内存管理的细节(包括它的内存分配的具体逻辑),但是每次从NioSocketChannel中读取数据时,应该分配多少内存去读呢? 例如,客户端发送的数据为1KB,若每次都分配8KB的内存数据,那么需要64次才能全部读完,对性能有很大的影响,那么对于这个问题,Netty是如何解决的呢?
NioEventLoop线程在处理OP_READ事件,进入NioByteUnsafe循环读取数据时,使用了两个类来处理内存分配,一个是ByteBufAllocator,PooledByteBufAllocator 为它默认实现类,另一个是RecvByteBufAllocator,AdaptiveRecvByteBufAllocator是它的默认实现类 ,在DefaultChannelConfig初始化时设置, PooledByteBufAllocator主要用来处理内存分配,并最终委托PoolArena去完成,AdaptiveRecvByteBufAllocator主要用来计算每次读取循环时应该分配多少内存,NioByteUnsafe之所有需要循环读取,主要是因为它分配的初始ByteBuf不一定能够容纳读取到的所有数据,NioByteUnsafe循环读取的核心代码解读如下 :
public final void read() { // 获取pipeline通道配置,Channel管道 final ChannelConfig config = config(); // socketChannel已经关闭 if (shouldBreakReadReady(config)) { clearReadPending(); return; } final ChannelPipeline pipeline = pipeline(); // 获取内存分配器,默认为PooledByteBufAllocator final ByteBufAllocator allocator = config.getAllocator(); // 获取RecvByteBufAllocator内部的计算器Handle final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle(); // 清空上一次读取的字节数,每次读取时均重新计算 // 字节buf分配器, 并计算字节buf分配器Handler allocHandle.reset(config); ByteBuf byteBuf = null; boolean close = false; try { //当对端发送一个超大的数据包时,TCP会拆包。 // OP_READ事件只会触发一次,Netty需要循环读,默认最多读16次,因此ChannelRead()可能会触发多次,拿到的是半包数据。 // 如果16次没把数据读完,没有关系,下次select()还会继续处理。 // 对于Selector的可读事件,如果你没有读完数据,它会一直返回。 do { // 分配内存 ,allocator根据计算器Handle计算此次需要分配多少内存并从内存池中分配 // 分配一个ByteBuf,大小能容纳可读数据,又不过于浪费空间。 byteBuf = allocHandle.allocate(allocator); // 读取通道接收缓冲区的数据 , 设置最后一次分配内存大小加上每次读取的字节数 // doReadBytes(byteBuf):ByteBuf内部有ByteBuffer,底层还是调用了SocketChannel.read(ByteBuffer) // allocHandle.lastBytesRead()根据读取到的实际字节数,自适应调整下次分配的缓冲区大小。 allocHandle.lastBytesRead(doReadBytes(byteBuf)); if (allocHandle.lastBytesRead() <= 0) { // nothing was read. release the buffer. // 若没有数据可读,则释放内存 byteBuf.release(); byteBuf = null; close = allocHandle.lastBytesRead() < 0; if (close) { // There is nothing left to read as we received an EOF. // 当读到-1时, 表示Channel 通道已经关闭 // 没有必要再继续 readPending = false; } break; } // 更新读取消息计数器, 递增已经读取的消息数量 allocHandle.incMessagesRead(1); readPending = false; // 通知通道处理读取数据,触发Channel管道的fireChannelRead事件 pipeline.fireChannelRead(byteBuf); byteBuf = null; } while (allocHandle.continueReading()); // 读取操作完毕 ,读结束后调用,记录此次实际读取到的数据大小,并预测下一次内存分配大小 allocHandle.readComplete(); // 触发Channel管道的fireChannelReadComplete事件 pipeline.fireChannelReadComplete(); if (close) { // 如果Socket通道关闭,则关闭读操作 closeOnRead(pipeline); } } catch (Throwable t) { // 处理读取异常 handleReadException(pipeline, byteBuf, t, close, allocHandle); } finally { // Check if there is a readPending which was not processed yet. // This could be for two reasons: // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method // // See https://github.com/netty/netty/issues/2254 if (!readPending && !config.isAutoRead()) { // 若操作完毕,且没有配置自动读 // 则从选择Key兴趣集中移除读操作事件 removeReadOp(); } } } }
RecvByteBufAllocator的默认实现类AdaptiveRecvByteBufAllocator 是实际的缓冲管理区, 这个类可以根据读取到的数据预测所需要的字节是多少,从而自动增加或减少,如果上一次读循环将缓冲区填充满了,那么预测的字节数会变大,如果连续两次循环都不能填满已经分配的缓冲区,则预测的字节数会变少。
AdaptiveRecvByteBufAllocator内部维护了一个SIZE_TABLE数组,记录了不同的内存大小,按照分配需要寻找最合适的的内存块, SIZE_TABLE数组的值都是2^n,这样便于软硬件进行处理,SIZE_TABLE数组的初始化与PoolArena中的normalizeCapacity的初始化类似,当需要的内存很小时,增长的幅度不大,当需要的内存较大时,增长的幅度比较大,因此在[16,512]区间每次增加16 , 直到512,而从512开始,每次翻一倍,直接int的最大值 。
当对内部计算器Handle的具体实现类HandleImpl进行初始化时,可根据AdaptiveRecvByteBufAllocator的getSizeTableIndex二分查找方法获取SIZE_TABLE的下标index并保存,通过SIZE_TABLE[index]获取下次需要分配的缓冲区的大小nextReceiveBufferSize并记录, 缓冲区的最小容量属于对于SIZE_TABLE中的下标为minIndex的值,最大容量属于对于SIZE_TABLE中的下标maxIndex的值及bool类型标识属性decreaseNow,这三个属性用于判断下一次创建缓冲区是不需要减少。
NioByteUnsafe每次读循环完成后会根据实际读取的字节数和当前缓冲区的大小重新设置下次需要分配的缓冲区的大小 , 具体代码如下。
// 循环读取完后被调用 public void readComplete() { record(totalBytesRead()); } private void record(int actualReadBytes) { if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT - 1)]) { if (decreaseNow) { // 若减少标识decreaseNow连续两次为true, 则说明下次读取字节数需要减少SIZE_TABLE下标减1 index = max(index - INDEX_DECREMENT, minIndex); nextReceiveBufferSize = SIZE_TABLE[index]; decreaseNow = false; } else { decreaseNow = true; // 第一次减少,只做记录 } } else if (actualReadBytes >= nextReceiveBufferSize) { // 实际读取的字节大小要大于或等于预测值 index = min(index + INDEX_INCREMENT, maxIndex); // SIZE_TABLE 下标 + 4 nextReceiveBufferSize = SIZE_TABLE[index]; // 若当前缓存为512,则变成 512 * 2 ^ 4 decreaseNow = false; } }
可以模拟NioByteUnsafe的read()方法,在每次读取循环开始时,一定要先重置totalMessages与totalByestRead(清零),读取完成后,readComplete会计算调整下次预计需要分配的缓冲区的大小, 具体代码如下 :
public class GuessTest { public static void main(String[] args) { AdaptiveRecvByteBufAllocator allocator = new AdaptiveRecvByteBufAllocator(); RecvByteBufAllocator.Handle handle = allocator.newHandle(); System.out.println("================开始I/O读事件模拟 ============"); // 读取循环开始前先重置,将读取的次数和字节数设置为0 // 将totoalMessages 与totalBytesRead置为0 handle.reset(null); System.out.println(String.format("第1次模拟读取,需要分配的大小为%d", handle.guess())); handle.lastBytesRead(256); // 调整下次预测值 handle.readComplete(); // 在每次读取数据时都需要重置totalMessages与totalBytesRead handle.reset(null); System.out.println(String.format("第2次模拟读,需要分配的内存大小:%d" , handle.guess())); handle.lastBytesRead(256); handle.readComplete(); System.out.println("==========连续读取的字节小于默认分配的字节数=========="); handle.reset(null); System.out.println(String.format("第3次模拟读,需要分配的大小为%d",handle.guess())); handle.lastBytesRead(512); // 调整下次预测值,预测值应该增加到512 * 2 ^ 4 handle.readComplete(); System.out.println("============读取的字节变大 ============="); handle.reset(null); // 读循环中缓冲区的的变大 System.out.println(String.format("第4次模拟读,需要分配的大小: % d", handle.guess())); } }
当然,关于这一块的具体源码我们在下一篇博客再来分析,当然,感觉本篇博客的内容和Netty内存分配好像还是没有具体的联系,请看我尾尾道来。
public ByteBuf directBuffer(int initialCapacity, int maxCapacity) { if (initialCapacity == 0 && maxCapacity == 0) { return emptyBuf; } validate(initialCapacity, maxCapacity); return newDirectBuffer(initialCapacity, maxCapacity); }
看到了这个方法,是不是有一种默名的熟悉的感觉,之前在分析例子时,不是见过这个方法不? 请看directBuffer()方法。
总结
分析到这里,如果有读者看到这里,我相信你也对Netty内存分配这一块也有了深刻的理解,我相信你也会被Netty巧妙的设计所征服,说真心话,真的设计得非常好,值得我辈学习, 这篇博客我也花了很长的时间在学习,公司最近业务也非常忙,写这篇博客也写得断断续续,但值得庆幸的是,我终于明白了,关于内存分配这一块源码分析也告一段落。 接下来会继续写Netty源码分析相关的源码,期待下一篇博客见,在写这篇博客的过程中也参考了其他的博客,我觉得也是非常写得好的。如果读者有兴趣,建议去读读,从哪里获得的知识点不重要,重要的是自己有没有学到东西。 话不多说了,期待下一篇博客见。
参考的博客
Netty源码解析(六)之PooledByteBufAllocator
参考书籍
《Netty源码剖析与应用-刘耀林》
代码地址
https://gitee.com/quyixiao/netty-netty-4.1.38.Final.git