创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家!
在上一篇文章我们介绍了Netty中的ByteBuf。ByteBuf是数据的存储介质,那么Netty是如何根据需要进行内存分配的呢?这是本篇文章要介绍的内容。
内存分配的本质
我们通过代码新创建的对象,本质上是以字节为单位存储在内存中,根据定义的类型在内存中申请相应大小的内存空间,将数据以字节为单位存储在内存中。但是这只是开辟了一个内存空间,具体要如何使用呢?操作系统会给新开辟的内存空间生成一个内存地址,暴露给程序进行调用。同时会给开辟的内存空间一个标识,标识该内存空间不可再分配了,只有在内存被回收之后,才会把标识修改为可以分配。
内存分配和回收是需要消耗系统性能的,所以在高并发场景下,不应该进行频繁的内存分配和回收操作。Netty4使用了内存池来管理内存的分配和回收,Netty内存池参考了Slab分配和Buddy分配思想。Slab分配是将内存分割成大小不等的内存块,在用户线程请求时根据请求的内存大小分配最为贴近Size的内存快,减少内存碎片同时避免了内存浪费。Buddy分配是把一块内存块等量分割回收时候进行合并,尽可能保证系统中有足够大的连续内存。
Netty 总体的分配策略如下。
-
为了避免线程间锁的竞争和同步,每个I/O线程都对应一个 PoolThreadCache,负责当前线程使用非大内存的快速申请和释放。
-
当从 PoolThreadCache 中获取不到内存时,就从 PoolArena 的内存池中分配。当内存使用完并释放时,会将其放到 PoolThreadCache 中,方便下次使用。若从 PoolArena的内存池中分配不到内存,则从堆内外内存中申请,申请到的内存叫 PoolChunk。当内存块的大小默认为16MB时,会被放入 PoolArea 的内存池中,以便重复利用。当申请大内存时(超过了 PoolChunk 的默认内存大小16MB),直接在堆外或堆内内存中创建(不归PoolArea管理),用完后直接回收。
Netty内存管理层级结构如下图所示,其中右边是内存管理的3个层级,分别是本地线程缓存、分配区arena、系统内存。左边是内存块区域,不同大小的内存块对应不同的分配区。
Netty内存分配的流程如下所示:
-
Netty在具体分配内存之前,会先获取本次内存分配的大小。具体的内存分配由PoolArena统一管理,先从线程本地缓存PoolThreadCache中获取,线程本地缓存采用固定长度队列缓存此线程之前用过的内存。
-
若本地线程无缓存,则判断本次需要分配的内存大小,若小于512B,则先从PoolArena的tinySubpagePools缓存中获取;若大于或等于512B且小于8KB,则先从smallSubpagePools缓存中获取,上述两种情况缓存的对象都是PoolChunk分配的PoolSubpage。若大于或等于8KB或在SubpagePools缓存中分配失败,则从PoolChunkList中查找可分配的PoolChunk。
-
若PoolChunkList分配失败,则创建新的PoolChunk,由PoolChunk完成具体的分配工作,最终分配成功后,加入对应的PoolChunkList中。若分配的是小于8KB的内存,则需要把从PoolChunk中分配的PoolSubpage加入PoolArena的SubpagePools中。
Netty最新版本基于jemalloc4算法进行内存分配,去掉了Tiny,保留了small、normal、Huge。
-
small :[0-28k]
-
normal:[28k-16M]
-
huge: > 16M
基于jemalloc4的Netty内存结构如下图:
Netty中和内存分配相关的类有PoolArena、ChunkList、Chunk、Page、SubPage。
PoolArena用来管理池化的内存结构,里面有一个 PoolSubpage 类型的数组,用来存储零散内存,和多个按照使用率划分的多个PoolChunkList 类型的对象,Netty把多个Chunk按照使用率存储到对应的PoolChunkList中,PoolChunkList是一个双向链表,把各种使用率的PoolChunkList联系起来。
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache threadCache) {
assert lock.isHeldByCurrentThread();
if (q050.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
q025.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
q000.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
qInit.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
q075.allocate(buf, reqCapacity, sizeIdx, threadCache)) {
return;
}
// Add a new chunk.
PoolChunk<T> c = newChunk(sizeClass.pageSize, sizeClass.nPSizes, sizeClass.pageShifts, sizeClass.chunkSize);
boolean success = c.allocate(buf, reqCapacity, sizeIdx, threadCache);
assert success;
qInit.add(c);
}
分配内存时为什么选择从 q050 开始
1、qInit 的 Chunk 利用率低,但不会被回收。
2、q075 和 q100 由于内存利用率太高,导致内存分配的成功率大大降低,因此放到最后。
3、q050 保存的是内存利用率 50%100% 的 Chunk,这应该是个折中的选择。这样能保证 Chunk 的利用率都会保持在一个较高水平提高整个应用的内存利用率,并且内存利用率在 50%100% 的 Chunk 内存分配的成功率有保障。
4、当应用在实际运行过程中碰到访问高峰,这时需要分配的内存是平时的好几倍需要创建好几倍的 Chunk,如果先从 q000 开始,这些在高峰期创建的 Chunk 被回收的概率会大大降低,延缓了内存的回收进度,造成内存使用的浪费。
PoolChunkList
PoolChunkList 负责将 PoolChunk 添加或移除出 PoolChunk 形成的双向链表。如果用户使用池化思想分配内存,Netty 直接会把数据存入到提前分配好的 PoolChunk 中,PoolChunk 中的可用内存容量是一直动态变化的,如果 PoolChunk 链表非常长,偏偏可以分配的 PoolChunk 在链表的最后,那么程序如何快速定位到合适的 PoolChunk 呢? Netty 的实现思路是在 PoolChunkList 中定义了 minUsage 和 maxUsage 两个成员变量,在 PoolChunkList 中按照最小使用率和最大使用率区间,来分组管理 PoolChunk 。如果每一个PoolChunk 内存块大小是16MB,现在需要6MB内存来存储数据,那么程序直接从内存利用率为0 - 50%的 PoolChunkList 链表中寻找即可,因为这个链表中每一个 PoolChunk 对象内存使用率最大只有50%,也就是说最少都还有8MB内存可以被分配,肯定可以满足6MB数据的内存存储需求。PoolChunkList 也是一个双向链表结构,它是按照最小使用率和最大使用率内存区间排序形成的,这样做的好处是让由于 PoolChunk 对象的可用内存发生改变时,可以快速移动到自己所属的最新的 PoolChunkList 对象上,方便接下来的程序内存分配。
PoolChunk
可以把Chunk看作一块大的内存,这块内存被分成了很多小块的内存,Netty在使用内存时,会用到其中一块或多块小内存。通过0和1来标识每个内存块的占用情况,通过内存的偏移量和请求内存空间大小reqCapacity来决定读/写指针。内存池在分配内存时,只会预先准备固定大小和数量的内存块,不会请求多少内存就恰好给多少内存,因此会有一定的内存被浪费。使用完后交还给PoolChunk并还原,以便重复使用。
PoolChunk内部维护了一棵平衡二叉树,默认由2048个page组成,一个page默认为8KB,整个Chunk默认为16MB,其二叉树结构如图所示。
在Netty的最新版本中,把page由run来管理,并存储在runsAvailMap这个结构中。每一个run的首个runOffset和最后runOffset存储在runsAvailMap中。runsAvail是一个整形优先级队列数组,存储的是handle。handle的格式如下图所示:
oooooooo ooooooos ssssssss ssssssue bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
o: chunk中的page偏移量, 15bit
s: run中的page数量, 15bit
u: isUsed 此区域是否被使用, 1bit
e: isSubpage 是否用于small规格的分配, 1bit
b: 给small规格使用的,poolSubPage内分配的内存都是同一容量大小,不能定义不同的大小。
如果poolSubPage分配到8192B,每一个内存块32B,那么总共可以分配256个,这256个可以
用一个bit数组来表示,bitmapIdx其实就是这个bit数组的索引下标, 如果不是small规格,
32位都是0。32bit