为了避免频繁的内存分配给系统带来负担以及GC对系统性能带来波动,Netty4使用了内存池来管理内存的分配和回收,Netty内存池参考了Slab分配和Buddy分配思想。Slab分配是将内存分割成大小不等的内存块,在用户线程请求时根据请求的内存大小分配最为贴近Size的内存快,减少内存碎片同时避免了内存浪费。Buddy分配是把一块内存块等量分割回收时候进行合并,尽可能保证系统中有足够大的连续内存。
Netty的内存分配算法无论是在原理还是具体实现上,都是高度复杂的。 为了完成内存分配,Netty在内部使用了cache(也即线程局部缓存,参见PoolThreadCache.java和PoolThreadLocalCache.java)、 Arena(参见PoolArena.java)、ChunkList(参见PoolChunkList.java)、Chunk(参见PoolChunk.java)、Page(实际是PoolChunk构造的二满叉树的叶子节点)、SubPage(参见PoolSubpage.java)等数据结构相互配置才完成了内存的高效率分配。
PooledArena是一块连续的内存块,为了优化并发性能在Netty内存池中存在一个由多个Arena组成的数组,在多个线程进行内存分配时会按照轮询策略选择一个Arena进行内存分配。一个PoolArena内存块是由两个SubPagePools(用来存储零碎内存)和多个ChunkList组成,两个SubpagePools数组分别为tinySubpagePools和smallSubpagePools。每个ChunkList里包含多个Chunk按照双向链表排列,每个Chunk里包含多个Page(默认2048个),每个Page(默认大小为8k字节)由多个Subpage组成。Subpage由M个”块”构成,块的大小由第一次申请内存大小决定。当分配一次内存之后此page会被加入到PoolArena的tinySubpagePools或smallSubpagePools中,下次分配时就如果”块”大小相同则由其直接分配
当利用Arena来进行分配内存时,根据申请内存的大小有不同的策略。例如:如果申请内存的大小小于512时,则首先在cache尝试分配,如果分配不成功则会在tinySubpagePools尝试分配,如果分配不成功,则会在PoolChunk重新找一个PoolSubpage来进行内存分配,分配之后将此PoolSubpage保存到tinySubpagePools中
尽管Netty的内存分配算法源码复杂晦涩难懂,但是使用者并不需要关心其底层实现。对于使用者来说,分配一个内存块只需要如下几行代码即可完成。(Netty内存分配包括了堆内存和非堆内存(Direct内存)的分配,但核心算法是类似的,下述代码仅仅展示了堆内存的分配方式)。
@Test
public void testPooledAllocator() {
PooledByteBufAllocator pooledByteBufAllocator = new PooledByteBufAllocator(false);
//这里: byteBuf的实际类型是:PooledUnsafeHeapByteBuf
ByteBuf byteBuf = pooledByteBufAllocator.heapBuffer(252);
System.out.println(byteBuf);
}
虽然上述分配堆内存的的代码看起来还是蛮简单的。但若要了解其底层实现,那么cachePoolThreadCache.java、PoolThreadLocalCache.java、PoolArena.java、PoolChunkList.java、PoolChunk.java、PoolSubpage.java等数据结构或类,任何一个netty源码的研究者,都是跳不过的。
本人2017年阅读netty源码的时候,并没有读懂。今年(2019)3月在Hypercube大神系列高质量博文(个人认为是全网分析最透彻的博文)的帮助下并辅助代码调试,基本理解了netty内存分配源码,现在也将相关文章列出来,供有需要的人查阅。
1. 自顶向下深入分析Netty(十)–JEMalloc分配算法
2.自顶向下深入分析Netty(十)–PoolSubpage
3. 自顶向下深入分析Netty(十)–PoolChunk
4. 自顶向下深入分析Netty(十)–PoolChunkList
5. 自顶向下深入分析Netty(十)–PoolArena
6. 自顶向下深入分析Netty(十)–PoolThreadCache
相信细心的读者在阅读完上述几篇文章(不再需要阅读其他人的相关文章)后基本能够理解Netty内存分配算法的原理和实现。那么本人写这篇的博文得目的是什么呢?
写作本文的目的就是: Hypercube大神在分析netty内存分配相关数据结构源码的时候,并没有将netty内存分配器PooledByteBufAllocator是如何协调PoolArena、PoolChunk、PoolChunkList、PoolSubpage等对象共同完成内存分配的过程串联起来。
所以,本文只为弥补一点点缺憾,本文将通过代码注释的方式,讲述PooledByteBufAllocator在调用两次 heapBuffer方法后发生的一些内部细节。
注: 继续往下看之前,请务必阅读上述博文并达到理解的程度,否则后续内容虽然很有意义,但对你本人来说,都是bullshit!!!
代码及解释如下,建议读者尽可能不要忽略代码中的每一行文字叙述
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import org.junit.Test;
public class PooledByteBufAllocatorTest {
/**
* 该方法仅仅用于解释PooledByteBufAllocator分配基于堆内存的ByteBuf的过程。
*
* byteBuf(这里的实际类型是PooledUnsafeHeapByteBuf)对象的属性memory是一个字节数组,
* byteBuf对象的memory字段与PoolChunk对象的memory字段指向同一个字节数组。我们可以
* 通过PooledUnsafeHeapByteBuf的初始化方法init0进行佐证。
*
* init0方法其实继承自其父类: PooledByteBuf.java. 该方法内部逻辑如下:
*
* private void init0(PoolChunk<T> chunk, ByteBuffer nioBuffer,
* long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
* assert handle >= 0;
* assert chunk != null;
*
* this.chunk = chunk;
* memory = chunk.memory;
* tmpNioBuf = nioBuffer;
* allocator = chunk.arena.parent;
* this.cache = cache;
* this.handle = handle;
* this.offset = offset;
* this.length = length;
* this.maxLength = maxLength;
* }
*
* 上述方法的语句: memory = chunk.memory表明, PoolChunk与 ByteBuf是共享底层字节数组的
*
*/
public static void main(String[] args) {
// 新建一个基于内存池的、堆内存分配器
PooledByteBufAllocator pooledByteBufAllocator = new PooledByteBufAllocator(false);
//尽管PoolChunk与 ByteBuf是共享底层字节数组的。但是这里的byteBuf对象申请的内存是252个字节,
// 规范化后是256字节。
// 256字节远小于1个pageSize(8192)的大小。属于 tiny分配。
//
//
// 所以PoolChunk的第1个Page会被等分为32个subpage(计算方式:pagesize / 第一次分配规范化后的大小
// = 8192/256),
// 在此处代码示例中, byteBuf占用第1个Page(注:第一个Page的id是2048)的第0个SubPage。
// 因为第1个Page占用的底层字节数组memory的位置范围是0 ~ 8193。
// 所以第1个subpage占用的底层字节数组memory的范围是:0 ~ 255,也即一个subpage的大小,
// 实际上byteBuf只需要252个字节,最大可以使用256个字节);
// 所以byteBuf对象只能使用memory[0] ~ memory[255] 范围的字节。
//
//
// 注: memory的大小是16M或者说16777216个字节。memory = new byte[8192 << 11]。
//其中pageSize = 8192; 11是默认的PoolChunk的高度。
//这里: byteBuf对象的实际类型是:PooledUnsafeHeapByteBuf
ByteBuf byteBuf = pooledByteBufAllocator.heapBuffer(252);
//byteBuf对象调用三次writeByte(1)方法后,底层字节数组memory[0] = 1, memory[1] = 1,
// memory[2] = 1, memory[3 ~ 16777215] = 0;
byteBuf.writeByte(1);
byteBuf.writeByte(1);
byteBuf.writeByte(1);
// byteBuf1申请的内存是8192个字节, 规范化后也是8192字节, 恰好是一个Page的大小。
// 所以本次分配属于normal分配, 不需要将Page切分为更小的subpage。
// byteBuf1实际占用的是PoolChunk的第2个Page(注意: 第2个Page的id是2049)。
// 因为第2个Page占用的底层字节数组memory的位置范围是8192 ~ 16383。
// 所以byteBuf1只能使用memory[8192] ~ memory[16383] 范围的字节(这个范围的大小恰好
//是一个pagesize的大小)
ByteBuf byteBuf1 = pooledByteBufAllocator.heapBuffer(8192);
//byteBuf1对象调用4次writeByte(1)方法后,底层字节数组状态如下:
// memory[0] = 1, memory[1] = 1, memory[2] = 1, memory[3 ~ 8191] = 0;
// memory[8192] = 1, memory[8193] = 1; memory[8194] = 1; memory[8195] = 1,
// memory[8196 ~ 16777215] = 0;
//
//
// memory状态变化成这个样子的原因是上述三次byteBuf.writeByte(1)调
//用导致: memory[0 ~ 2]被byteBuf对象顺序写入了三个字节;
// 而下述四次byteBuf1.writeByte(1)调用导致: memory[8192 ~ 8195]被byteBuf1对象
//顺序写入了四个字节
byteBuf1.writeByte(1);
byteBuf1.writeByte(1);
byteBuf1.writeByte(1);
byteBuf1.writeByte(1);
System.out.println("ref count: " + byteBuf1.refCnt());
//调用release方法后,byteBuf1的引用计数变为0。 byteBuf1对象的memory等属性会被置为null,
//然后被回收到对象池中(netty对象池的实现可以参考本人博文:
//netty-对象池实现Recycler用法测试(https://blog.csdn.net/nmgrd/article/details/73302246) )。
System.out.println(byteBuf1.release());
System.out.println(byteBuf1.refCnt());
}
}