可见在学习Netty的时候ByteBuf随处可见,但是如何高效的分配ByteBuf还是很复杂的。Netty的池化内存分配还是比较难的。很多人研究过,看过,但还是一头雾水。是的,这篇文章主要是讲解:Netty分配pooledoff-heap内存的细节,期待让你明白!!!因为为了更好的表达,文中的图我至少画了6个小时,图不够好。熟悉了,还要强调一些细节。由于源码涉及到很多二进制操作,建议阅读其他二进制文章。ByteBuf的重要性ByteBuf在Netty中一直存在,读写必备!ByteBuf是Netty的数据容器,高效分配ByteBuf非常重要!Netty从套接字中读取数据。Netty准备将数据写入套接字。从这里我们可以看出,在向socket写入数据之前,会判断是否是堆外内存。如果没有,它将构造一个directbuffer对象。详细代码如下:所以本文主要讲解:Netty分配池堆外内存的细节,其实很多分配堆内存的细节都是类似的。备注:一直没明白为什么要把堆外内存调到堆外内存。为什么要加这个判断呢?概述这次主要讨论的是池化内存的分配。PooledByteBufAllocator是netty分配池化内存的操作入口。它提供了常用的对外操作的API:Netty在发送数据的时候会判断是否是堆外内存,如果不是则进行封装:这里我们以分配pooled堆外内存为例进行说明文章。池化堆内存分配的过程其实也是类似的。我们先看一下配置示例demo:以后我们会根据这个简单的demo来分析。操作入口类PooledByteBufAllocator的初始化:进入后可以看到一个核心类的初始化操作:分配理论是jemalloc,可以理解为jemalloc在java版本中的实现。PoolThreadCache通过上图可以清楚的了解PoolThreadCache的主要数据结构。一开始,这些缓存中没有任何价值。只有在调用free释放时(在后续内存释放中会说明),才会将之前分配的内存大小放到缓存的队列中。其实每次分配的时候都会去检查缓存中是否有,如果有直接返回,如果没有,再进行正常的分配流程(内存分配后面会有说明)。看一下PoolArena的内容directArena:看一下PoolArena的结构。PoolArena通过下图可以清楚的了解PoolArena的主要数据结构。在PoolArena中,PoolChunkList和PoolSubpage对应的结构是PoolChunk和PoolSubpage。让我们详细看看这两件作品。第一次使用PoolChunk时,PoolChunkList和PoolSubpage是默认值,需要添加一个新的Chunk。Chunk的默认值为16M。内部结构是一棵完全二叉树,共有4096个节点,2048个叶子节点(每个叶子节点的大小为一页,为8k)。非叶子节点的内存大小等于左子树的内存大小加上右子树的内存大小。完整的二叉树结构如下:这个完整的二叉树在java中用一个数组来表示。唯一需要注意的是,下标是从1开始的,而不是0。depthMap的值在??初始化后不会改变,memoryMap的值会随着节点的分配而改变。如果数值太多,我就不截图了。就是用数组来表示上面的完整二叉树,但是存储的值不是节点的下标,而是存储树的深度。depthMap数组的值为0,表示可以分配16M空间,如果为1,则可以分配8M,如果为2,则可以分配4M,如果为3,则可以分配2M……………………如果是11,可以分配8k空间。如果已经分配了节点,就设置为12即可。如何确定深度处要分配的大小呢?如果归一化后要分配的内存小于8k,那么可以分配到8k以上(即深度为11)。如果是8k或者大于8k,那么深度可以通过下面的代码定位:intd=maxOrder-(log2(normCapacity)-pageShifts);知道深度后,如何定位节点???找到节点后,首先显示该节点的占用情况,然后更新父节点的parent...如下:SubpagePool上图是关于SubpagePool的内存结构。我们在分配page的时候,是不是根据memoryMap的值来分配的,那么如果是subpagePool呢?subpagePool分为两类:tinySubpagePools和smallSubpagePools。大小也在上图中,每个类别都是固定的。从大小上来说,如果分配了256b的大小,那么一个page就是8k,8*1024/256=32块。那么怎么表示each已经分配了呢?privatefinallong[]位图;由于long占用的字节数是64,我们这里只需要表示32,就可以使用long了,二进制中的每一位表示1已经使用,0表示还没有使用。由于子页面不仅需要定位到完全二叉树的节点,还需要知道序号和long的序号,所以比较复杂:用long的前32位表示序号子页面的long上面几位,通过最后32来表示完全二叉树上的节点,完美。Allocation核心分配入口:ByteBufbyteBuf=alloc.directBuffer(256);后续代码:让我们看看:PooledByteBufbuf=newByteBuf(maxCapacity);构建一个PooledByteBuf对象。最后返回PooledByteBuf对象。我们看一下类继承结构:AllByteBufbyteBuf=alloc.directBuffer(256);这句话没有错,也不会报错。下面看一下newByteBuf(maxCapacity)的详细实现:这里使用了Netty实现的Recycler对象池技术。Recycler的设计也很精致。以后可以专门写一篇Recycler的文章。今天不是重点。我们只需要知道分配PolledByteBuf对象的成本有点高。如果需要频繁使用PolledByteBuf对象,对性能有要求,那么池化技术是一个不错的选择(比如我们之前使用的线程池和数据库连接池都是类似的),池化技术减少了频繁创建对象带来的性能开销在某种程度上。其实这种类似的思路很常见(比如查询数据库成本高,缓存到redis的思路也是一样的),后续也可以体会本文(PoolThreadCache)。通过PooledByteBufbuf=newByteBuf(maxCapacity);只是得到一个初始对象。分配的核心是:allocate(cache,buf,reqCapacity);先在步骤1尝试分配,根据不同的类型定位不同的缓存,有分配直接返回。如果无法分配第1步,则继续执行上面的第2步。2步分配详解:查看需要分配什么类型的page或subpage,如果是subpage,查看是tinySubpagePools还是smallSubpagePools,找到对应的slot,查看链表中是否有可用的PoolSubpage,进行分配如果是的话修改标记退出。如果没有,则需要先分配一个页面。根据chunklist,看看有没有合适的。如果有合适的,则在这些已有的chunk上分配一个page(pages的分配也是如此。up)。之后根据分配的page,分配请求的size(因为一个page可以存放很多相同size的数量)需要用long位标记,表示分配位置,parent等值完全二叉树被修改,分配结束。如果没有chunk,则需要分配一个新的chunk,重复上述步骤。释放核心释放入口:byteBuf.release();后续代码:通过这段代码,我们将这一段放入对应的队列中:缓存在对应的Cache队列中。
