在学习Netty的时候,ByteBuf随处可见,可是如何高效分配ByteBuf仍是很复杂的,Netty的池化内存分配这块仍是比较难的,不少人学习过,看过可是仍是云里雾里的,本篇文章就是主要来说解:**Netty分配池化的堆外内存的细节,**期待可让你明白!!!html
因为为了更好的表达,文章中的图我最少画了6小时,画的不熟悉,而且也强调一些细节上。java
因为该源码中涉及到大量的二进制操做,建议看看我以前写的2篇二进制文章:java二进制相关基础,二进制实战技巧。git
ByteBuf在Netty中一直存在,读写必备!ByteBuf是Netty的数据容器,高效分配ByteBuf相当重要!github
Netty从socket读取数据。redis
经过这里咱们就能够看到,再把数据写socket的以前会判断是不是堆外内存,若是不是会构造一个directbuffer对象的,细节代码以下:数据库
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
if (buf.isDirect()) {
return msg;
}
return newDirectBuffer(buf);
}
复制代码
因此本篇文章就是主要来说解:**Netty分配池化的堆外内存的细节,**其实分配堆内存的细节不少也是相似的。api
备注: 为何不是堆外内存还要转堆外内存,为何加这个判断,我以前也不理解,突然有天和涤生大佬讨论,讨论讨论就清晰了,后续有空写篇。数组
本次主要讨论的是关于池化内存的分配,PooledByteBufAllocator就是netty分配池化内存的操做入口。缓存
其提供对外经常使用操做api:bash
Netty在发送数据的时候会判断是不是堆外内存,若是不是会进行封装的:
全部这里咱们以**分配池化的堆外内存为例,进行本文说明。**池化的堆内存分配其实流程都差很少的。
下面咱们来看看分配示例demo:
public static void main(String[] args) {
ByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;
//tiny规格内存分配 会变成大于等于16的整数倍的数:这里254 会规格化为256
ByteBuf byteBuf = alloc.directBuffer(254);
//读写bytebuf
byteBuf.writeInt(126);
System.out.println(byteBuf.readInt());
//很重要,内存释放
byteBuf.release();
}
复制代码
后续咱们都会根据这段简单的demo进行分析。
PooledByteBufAllocator的初始化:
进去以后能够看到核心类的一初始化操做:
分配理论是jemalloc,能够理解为java版本的jemalloc实现。
经过上图能够清晰的了解到PoolThreadCache的主要数据结构。
开始的时候,这些Cache里面都是没有值的,只有在调用free释放的时候(在后续释放内存中会讲解),才会把以前分配的内存大小放到该cache的queue里面,其实每次分配的时候都是先看看是否缓存里面有,若是有直接返回,没有则进行正常的分配流程(内存分配会讲解)。
咱们来看看PoolArena directArena内容:
下面咱们来看看PoolArena结构。
经过下图能够清晰的了解到PoolArena的主要数据结构。
在PoolArena里面涉及到PoolChunkList和PoolSubpage对应的结构有PoolChunk和PoolSubpage,咱们来详细的看看这2块内容。
第一次的时候,PoolChunkList、PoolSubpage都是默认值,须要新增一个Chunk,默认一个Chunk是16M。内部会结构是彻底二叉树一共有4096个节点,有2048个叶子节点(每一个叶子节点大小为一个page,就是8k),非叶子节点的内存大小等于左子树内存大小加上右子树内存大小。
彻底二叉树结构以下:
这颗彻底二叉树在java中是使用数组来进行表示的。
惟一须要注意的是,下标是从1开始而不是0.
depthMap
的值初始化后再也不改变,memoryMap
的值则随着节点分配而改变。
这个值太多就不都截图了,就是把上面那颗彻底二叉树用数组表示了而已,只是值存的不是节点的下标而是存的树的深度而已。
depthMap数组值为0表示能够分配16M空间,若是为1 表示能够分配8M,,若是为2表示嗯能够分配4M,若是为3表示能够分配2M ……………………若是为11表示能够分配8k空间。
若是该节点已经分配完成,就设置为12便可。
怎么肯定须要分配的大小在深度是多少?
若是须要分配的内存规格化以后,是小于8k,那么在8k上面分配便可(即深度为11)。
若是为8k或者大于8k那么经过下面代码就能够定位到深度了:
int d = maxOrder - (log2(normCapacity) - pageShifts);
复制代码
知道深度以后,怎么进行定位到那个节点呢???
找到该节点以后,先把该节点显示占用,在更新起父节点父节点的父………………以下:
上面的图就是关于SubpagePool的内存结构了。咱们在分配page的时候,根据memoryMap对于的值就知道是否被分配了,那么若是是subpagePool呢?
subpagePool分为2类:tinySubpagePools和smallSubpagePools,大小对于也对于上面的图里面了,每类都是固定大小的,若是分配256b的大小,那么一个page就是8k,8*1024/256 = 32块。那么怎么怎么表示每一个还被分配了呢?
private final long[] bitmap;
复制代码
因为一个long占64位,咱们这里仅仅是须要表示32个,因此使用一个long便可了,二进制每位 1表示已经使用了,0表示还未使用。
因为subpage不只仅须要定位到彻底二叉树在那个节点,还须要知道在long的第几个 而且是第几位,因此要复杂一些:
经过一个long的前32位来表示subpage的第几个long的第几位上面,经过后32来表示在彻底二叉树的那个节点上面,完美。
分配入口:ByteBuf byteBuf = alloc.directBuffer(256);
进行跟进代码:
咱们来看:PooledByteBuf buf = newByteBuf(maxCapacity);
构建PooledByteBuf对象。最后返回PooledByteBuf对象。
咱们来看下类继承结构:
全部ByteBuf byteBuf = alloc.directBuffer(256);这句话是没有什么问题的,不会报错。
咱们来看看newByteBuf(maxCapacity)的细节实现:
这里借助了Netty增长实现的Recycler对象池技术。Recycler设计也很是精巧,后续能够专门写篇Recycler文章,今天不是重点,咱们只要知道因为分配PolledByteBuf对象的代价有点大,若是须要频繁使用到PolledByteBuf对象,而且对性能有所要求,那么池化技术是一个不错的选择(好比咱们之前使用的线程池、数据库链接池等都是相似道理),**池化技术在必定程度上面减小了频繁建立对象带来的性能开销。**其实这个相似的思想很是常见(好比咱们查询数据库成本高,缓存到redis,思路也是同样的),在本篇后续中还能够体会到(PoolThreadCache)。
经过PooledByteBuf buf = newByteBuf(maxCapacity);仅仅是获取到了一个初始对象而已。
分配的核心在:allocate(cache, buf, reqCapacity);
以后在根据分配到的page,进行该请求大小的分配 (因为一个page能够存储不少同大小的数量)须要用long的位标记,表示该位置分配了,而且修改彻底二叉树的父等值,分配结束。若是没有chunk那么须要新分配一块chunk以后重复上面步骤便可。
释放入口 : byteBuf.release();
进行跟进代码:
经过这段代码咱们就这段放入到相应的queue了:
缓存到了对应的Cache的queue里面了。
文章github源代码地址:nettydemo,或者公号回复“Netty”获取源码地址。
若是读完以为有收获的话,欢迎点赞、关注、加公众号【匠心零度】,查阅更多精彩历史!!!