netty - PooledByteBufAllocator内存分配示例及解释


为了避免频繁的内存分配给系统带来负担以及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());

    }


}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值