深入Netty的缓冲区分配与管理-Special ByteBuffer

Posted 京东虚拟平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入Netty的缓冲区分配与管理-Special ByteBuffer相关的知识,希望对你有一定的参考价值。

作为一个NIO通讯框架,Netty在网络传输过程中要使用大量的缓冲区(ByteBuf)来进行数据的接收和发送,但是JDK API提供的ByteBuffer类在使用过程中操作方式比较复杂,特别是标志字段多且其含义晦涩,使用时容易出错,为了提高内存使用效率,方便操作,Netty实现了一套自己的缓冲区即ByteBuf。


1

ByteBuffer与ByteBuf

ByteBuffer的基本结构

① capacity,ByteBuffer最大能容纳的Byte个数,一般通过allocate或wrap方法分配后不能改变;

② position,读写开始位置,对ByteBuffer的读写字操作从此位置开始;

limit,读写边界,读写ByteBuffer的时候当position到达limit的位置时即表示读完或写满;

④ mark,position位置的标记,用来在未来的某一时刻恢复position位置。

ByteBuffer的操作及内部状态

① put操作,写入字节,下面是一个典型的写入3个字节后的ByteBuffer字节排布(蓝色表示有效字节)

② flip,读之前需要调用flip方法,也就是将position复位到0,并将limit设置到position的位置(注:调用flip时mark会复位-1)

深入Netty的缓冲区分配与管理-Special ByteBuffer

③ get,读取字节数据,会从position位置开始,下图是读取两个字节后的排布

深入Netty的缓冲区分配与管理-Special ByteBuffer

④ clear,将position位置设为0,limit设为capacity,并不清除内容

深入Netty的缓冲区分配与管理-Special ByteBuffer

可以看到ByteBuffer的标志总共有4个,状态切换比较多,在使用中要时刻关注这些状态对应的标志位,而且以上只是最基本的读写操作,还没有考虑压缩和容量扩展。

ByteBuf的内部结构

① ByteBuf通过两个指针的协作来完成读写操作,读操作使用readerIndex,写操作使用writerIndex;

② 初始状态的readerIndex、writerIndex值都是0,写入数据时writerIndex增加,读取数据时readerIndex增加;

③ 读写过程中readerIndex不会超过writerIndex,介于readerIndex和writerIndex的是已写入但未读取的字节。

ByteBuf的基本操作及内部状态

①  set/writeXX写入3个字节后的状态

深入Netty的缓冲区分配与管理-Special ByteBuffer

②  get/readXX读取两个字节后的状态

深入Netty的缓冲区分配与管理-Special ByteBuffer

我们可以看到Netty提供的ByteBuf比JDK原生的ByteBuffer更简单易用,内部字段的含义也比较明确

ByteBuf的类型

① UnpooledHeapByteBuf,在堆内存上分配的非池化的ByteBuf,使用后直接丢弃,不会重复使用

② PooledHeapByteBuf,池化的在堆内存中分配的 ByteBuf,使用后会被回收重复利用

③ UnpooledDirectByteBuf直接内存中分配的ByteBuf,不会被重复使用

④ PooledDirectByteBuf,池化的直接内存中分配的ByteBuf,使用后放入池中可以被重复使用

2

ByteBuf的分配

ByteBufAllocator

为了减少分配和释放内存的开销,Netty 通过ByteBufAllocator类来分配堆或直接内存池化的ByteBuf,在业务代码中我们可以通过以下方式来得到 ByteBufAllocator 的引用

Channel channel = ...;

ByteBufAllocator allocator = channel.alloc();

//或

ChannelHandlerContext ctx = ...;

ByteBufAllocator allocator2 = ctx.alloc();


如果业务代码中无法引用 ByteBufAllocator,Netty 提供一个实用工具类Unpooled来分配非池化的ByteBuf

ByteBufAllocator allocator = Unpooled.wrappedBuffer()

....


在IO处理层我们可以在初始化Channel时配置Allocator来应用具体的ByteBuf类型

bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

....



3

ByteBuf背后的内存组织

内存管理单位

① Netty将内存划分为Arena、ChunkList、Chunk、Page、Subpage共5个层次, 在Netty中并不存在Arena这个类,内存区域的管理是由PoolArena类来实现的,下面提到的Arena都指PoolArena;

Page是内存的最小分配单位,默认Page的大小是8K,可以通过配置启动参数设定;

Subpage是内存最小使用单位,一个Page可以包含0个或多个Subpage,Subpage的大小取决于首次从该Page分配内存的大小,如果首次在该Page中需要分配1k字节,那么该Page就被分为8个Subpage,每个Subpage大小为1k;

Chunk是由连续的Page组成,默认Chunk的大小是16M,也就是说1个Chunk有2048个Page;

⑤ ChunkList包含多个Chunk,每个ChunkList里包含的Chunk数量会动态变化;

⑥  Arena代表1个内存区域,在 Netty中内存池是由多个Arena组成的数组(可以通过运行参数配置Arena的个数),分配时会为每个线程按照轮询策略选择1个Arena,需要注意的是多个线程可能共用一个Arena。

自顶向下的组织策略

① 1个Arena由2个PoolSubpage数组和6个ChunkList组成,两个PoolSubpage数组分别为tinySubpagePools和smallSubpagePools,6个ChunkList是按照其中Chunk的内存利用率来划分的,每个ChunkList是一个双向链表结构;

深入Netty的缓冲区分配与管理-Special ByteBuffer

② tinySubpagePools和smallSubpagePools都是链表数组结构,tinySubpagePools用来保存大小为16-496B(16->32->48->…->496)的内存块链表,共有32个这样的链表,而smallSubpagePools用保存512-4096B的内存块链表,每个链表分配的内存块大小成倍增长(512->1024->2048->4096),数组长度为4;

深入Netty的缓冲区分配与管理-Special ByteBuffer


③ 每个ChunkList里包含多个Chunk,每个Chunk里包含多个Page(默认2048个),每个Page(默认大小为8K字节)由多个Subpage组成;

深入Netty的缓冲区分配与管理-Special ByteBuffer

④  Chunk的内存利用率会随着内存的分配和回收而发生变化,所以每个ChunkList里包含的Chunk数量会动态变化:

  • qInit:内存利用率0-25%的Chunk

  • q000:内存利用率1-50%的Chunk

  • q025:内存利用率25-75%的Chunk

  • q050:内存利用率50-100%的Chunk

  • q075:内存利用率75-100%的Chunk

  • q100:内存利用率100%的Chunk

4

ByteBuf的分配算法

内存大小规格化及分配策略

① 分配ByteBuf时会将申请的大小规格化为2的幂次方并且最小为16B,也就是说如果我们申请19B,规格化后的大小为32B,如果我们申请8B,规格化后的大小为16B;

②分配时对于小于页大小的内存,会在tinySubpagePools或smallSubpagePools中分配,tinySubpagePools用于分配小于512字节的内存,smallSubpagePools用于分配大于512字节小于页的内存;

③ 对大于pageSize小于chunkSize的内存分配请求,会在PoolChunkList的Chunk中分配。

分配过程中

① 为了在Chunk中快速搜索满足大小的内存,Chunk以2048个页面为基础,自底向上,每一层节点作为上一层的子节点构造出一棵满二叉树,用一个数组来表示二叉树,数组元素的值为结点所在的深度,结点旁边的数字为数组的下标,结点内的数字为数组元素的值即深度,为了表达深度和数组下标的关系(n=2的d次方),元素[0]不使用,每个叶子结点标志一个Page的使用状态,根结点就标识整个Chunk的使用状态,例如初始状态,[512] =9(等于结点所在的深度),表示512节点下所有的子节点都未分配过;

深入Netty的缓冲区分配与管理-Special ByteBuffer

② page0分配之后[512] =10(大于结点所在的深度), 则表示512节点下有子节点已经分配过,该节点不能直接被分配,而其子节点中的第10层和10层以下还存在未分配的节点;


深入Netty的缓冲区分配与管理-Special ByteBuffer

③ page0和page1都被分配后[512] = 12 (即总层数 + 1), 可分配的深度已经大于总层数,  则该节点下的所有子节点都已经被分配。

5

总结

① 在使用方式和应用场景上,ByteBuf比JDK原生的ByteBuffer更简单易用,由于摆脱了堆内存的约束,其提供的功能更加灵活多样 ;

② Netty对ByteBuf的使用和管理统筹了堆内存和直接内存,提供了比较完善的数据结构和算法来支撑对ByteBuf的分配和管理,在高并发的场景下可以极大的提高内存的使用效率。






以上是关于深入Netty的缓冲区分配与管理-Special ByteBuffer的主要内容,如果未能解决你的问题,请参考以下文章

netty源码之内存池

netty源码之内存池

netty源码之内存池

netty源码之内存池

Netty中线程封装与管理

Netty 学习笔记四 了解缓冲区