netty源码解读六(内存池相关)

申请内存到底是申请什么?

申请的地址,包括,某块内存地址+本次使用偏移量+更具体的偏移量(如果有必要);业务拿到该地址后就可以将数据存放到该处,使用结束后再归还;
PooledByteBufAllocator#newDirectBuffer方法中主要调用ByteBuf buf = directArena.allocate(xxx),而该方法中主要调用allocate(xxx)方法;

allocateNormal方法新建一个chunk
// Add a new chunk.
PoolChunk c = newChunk(pageSize, nPSizes, pageShifts, chunkSize);
boolean success = c.allocate(buf, reqCapacity, sizeIdx, threadCache);
assert success;
qInit.add©;

newChunk

1)调用了PoolChunk类的构造方法,而该构造方法中又调用了ByteBuffer.allocateDirect方法申请了16mb内存,即返回了jdk的ByteBuffer对象,赋值给了memory属性,因此poolChunk实例管理着16mb内存;尽管物理上PoolChunk是一个16M的内存空间,但逻辑上会按照树状结构来维护:关于这颗树有几点要说明:
• 1.1)PoolChunk会按照层数将16M的内存等分,第0层1个16M,第一层2个8M,第三层4个4M,依次类推直到第十一层,分成了2048个8K;
• 1.2)叶子节点的大小为8K;
• 1.3)为了快速找到节点层数,大小等关系,PoolChunk里维护了两个数组,depthMap维护了节点的深度值,初始化后不能改变;memoryMap的值和depthMap完全相同,只是memoryMap会改变,表示该节点是否可用;深度值从0开始,最底层的深度值为11;
在这里插入图片描述

1.4)PoolChunk中实际上并没有维护存储节点大小的二叉树,而是维护了如上图存储各节点深度值的二叉树,分配内存的时候,总是根据需要的内存大小定位到深度值,然后在memoryMap中寻找合适的节点;

Netty 4.1.61与4.1.45在内存池方面有一些区别,4.1.61中虽然利用的是LongLongHashMap和LongPriorityQueue,没找到memoryMap和deptMap,但是其实本质还是利用满二叉树实现的;memoryMap表示树上每个节点的分配能力值,数组长度为最多能分配的SubPage数目的两倍;满二叉树如何用数组表示呢?若父节点的索引为i,则左子节点的索引为2i,右子节点的索引为2i+1;数组的每个元素表示当前i下标的节点的可分配内存能力值,同一深度的树节点可分配能力值一样。如memoryMap[1]=0,表示根节点可分配内存能力值为0,该值越小,表示可分配能力越大,即根节点可分配16mb,memoryMap[2]=1,表示深度为1的节点可分配内存能力值为1,表示可分配8mb;memoryMap[0]空着不用;最终memoryMap数组被初始化为[0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3…];当 memoryMap对应索引的树节点其管理的内存被占用后,该索引位置的值会发生改变;
deptMap的值表示当前索引的树节点所在树的深度是多少,初始化后,deptMap就不会发生改变;

PoolChunk#allocate

判断请求是tiny,small类型的还是[8kb,16mb]的申请;

(4kb,16mb]的申请

1)先对申请内存的大小进行规格化得到一个2的幂大小的规格,接着计算 (4kb,16mb]的内存申请需要去满二叉树上哪一深度才能满足要求,求出深度值d;
2)再根据深度d定位到具体那一层的哪个id空闲,并将其占用,设置已使用标记,即设置被占用的节点的深度能力值为12,表示该节点已经被全部使用,无法再继续分配了;
3)最后修改所选id的父节点的分配能力值+1,是否加1这个要看父节点被影响的程度是否超过50%,如果影响部分小于50%,则只在第一次加1,若影响部分超过50%,则每影响一次就加1,之所以是50%,是因为[8kb,16mb]之间的分配,会默认按照2的n次幂的内存大小给申请者,比如16mb,被分配出去2mb和4mb最后结果是一样的,即16mb所在节点剩下只能分配8mb了,所以分配能力值需要加1;另外需要修改memoryMap数组中被占用的相应位置值为12;12代表该点已无法再分配了,因为深度为11时,已经是最小分配单位了;
4)组装handle,代表是申请[8kb,16mb]规格,低32位代表二叉树上id;
比如一个线程先申请3M的内存,接着申请2M,过程如下:
a)确定内存大小,为了便于管理,对于>=8K的内存,Netty会默认返回2的n次幂的内存大小给申请者,所以Netty会申请4M的内存给调用者;
b)计算深度值:log2(16M) - log2(4M) = 24 - 22 = 2(统一变为kb进行计算);
c)根据深度值去memoryMap中查找合适的节点,并循环更新上层节点的值,新的值为左右子节点中较小的值(因为规定较小的值代表更大的分配能力);
d)申请2M的内存,重复b,c步;步骤如下图所示,找到合适的节点后,将该节点的值更新为12;
在这里插入图片描述

tiny,small类型的申请[16b,4kb]

同样也需要进行规格化,如申请10b,变为申请16b;16b,32b,48b…496b为tiny类型(每次递增16b),512b,1024b,2kb,4kb为small类型(每次两倍递增);均小于8kb(一页),所以需要去满二叉树上的叶子节点中找一个可用的叶子节点即创建SubPage实例,并且将该实例放入PoolSubPage数组中对应的下标处;满二叉树的叶子节点从2048到4095刚好对应PoolSubPage数组从0到2047,目的是存放PoolChunK创建出来的SubPage;

1) 找到head,去tinySubpagePools和smallSubpagePools数组中找得到head节点,如下图所示;
在这里插入图片描述

2)计算当前叶子节点的管理内存在整个内存的偏移位置,以深度11作为入参,找分配能力为8kb的节点id号;计算该节点的runOffset,等于shift乘以runLength,假设节点id为2049,则得到2049的shift为1,runLength为8kb,所以得到runOffset为1乘以8kb得到8kb;假设节点id为2048,则得到2048的shift为0,runLength为8kb,所以得到runOffset为0乘以8kb得到0;2050的runOffset为16kb,2051的runOffset为24kb;(runOffset相当于起点地址)

3)创建PoolSubPage类型对象,用于管理PoolChunk创建出来的SubPage即一页;首先调用其构造函数,6个非常重要的入参,依次是head节点,PoolChunk实例,当前SubPage实例对应叶子节点的id号,当前叶子节点的管理内存在整个内存的偏移位置,一页大小(8kb),申请的大小(调整后的);构造方法中还会初始化位图,将一页按最小16b进行切割,即8kb/16b=512,每一位为1表示被占用,为0表示没有被占用,512/64=8,即用8个long型的变量即可表示连续的512位,此时用数组可以表示long bitMap = new long[8];
4)在构造方法中,会初始化位图bitMap,若申请规格为32b,则一页8kb/32b为256位,需要4个long型,则初始化bitMap数组的前四位为0;若申请规格为48b,则一页8kb/48b为170,需要3个long型,则初始化bitMap数组的前三位为0;
5)subPage.allocate方法中正式申请small,tiny规格内存
a)先从bitMap中查找一个可用的bit(即找到第一个为0的位),返回改bit的索引值;bitMap[0]代表0-63的索引,bitMap[1]代表64到127的索引…
b)再将该bit位置1;代表该快内存被占用了;
c)组装handle值,该值最高位为1用于区分[8kb,16mb]规格的申请,最高位为1代表是申请small,tiny规格,高32位代表申请的内存在bitMap上索引值(每次只占一个索引,不可能占多个索引),而低32位代表二叉树上占用节点的id;这里之所以要弄一个最高位为1,就是为了快速区分;
具体分配策略,参考如下文章
https://www.jianshu.com/p/aa2bb182466e

initBuf方法

根据上述两种情况返回的handle初始化buf;
若申请的是[8kb,16mb],则相对简单;
若申请的是small,tiny类型的内存,则需要根据bitMap已使用的索引值计算subPage的内部使用偏移量,公式是索引值乘以每位代表的大小,计算完之后,subPage的内部使用偏移量加subPage的runOffset就得到subPage在整个PoolChunk实例上的偏移量。
申请内存代码流程(以堆外内存为例)
2021年11月份分析的:
此次分析比9月份要更为完整清晰。从AbstractByteBuf#readBytes(int length)方法开始,其中会执行AbstractByteBufAllocator#buffer方法,其中又会执行directBuffer方法,其中又会执行PooledByteBufAllocator#newDirectBuffer方法;其中会执行如下几个方法:

1)threadCache.get()

拿到PoolThreadCache实例cache;

2)cache.directArena

拿到PoolArena类型的属性directArena;

3)directArena.allocate方法

3.1)newByteBuf方法

到ByteBuf 对象池内 获取一个空闲 ByteBuf 对象,并且重置ByteBuf 对象的 字段 等信息。

3.2)allocate方法;
3.2.1)normalizeCapacity(reqCapacity);

将reqCapacity转换为符合规格的大小normCapacity;

3.2.2)isTinyOrSmall(normCapacity)方法,判断申请规格是tiny或small
  3.2.2.1)cache.allocateTiny

若申请的是tiny规格,则从tiny缓存中拿;
3.2.2.2)cache.allocateSmall
若申请的是small规格,则从small缓存中拿;
3.2.2.3)PoolSubpage head = table[tableIdx];
从tinySubpagePools或smallSubpagePools数组中拿;
3.2.2.4)allocateNormal
3.2.2.4.1)PoolChunkList#allocate
尝试从list集合中拿poolChunk,有的话则遍历poolChunkList,执行PoolChunk#allocate方法,申请成功后,再次判断当前poolChunk的使用率,将其转至合适的poolChunkList中;
3.2.2.4.2)newChunk
拿到PoolChunk实例c;
3.2.2.4.3)c.allocate从新创建的PoolChunk内 分配内存
3.2.2.4.3.1)allocateRun
申请规格不小于一页8kb,调用此方法;
a)求申请规格的深度值
深度值=log2(16mb)-log2(normCapacity);假如申请的是3mb,规格化后为4mb,所以4mb在满二叉树的深度值为log2(16mb)-log2(4mb)=24-22=2;
b)allocateNode(d)方法查找合适的节点
c)更新当前poolChunk的空闲大小freeBytes
直接根据当前分配的节点的id计算内存大小,再减掉,freeBytes初始值为16mb;之所以要计算剩余空闲内存大小,是因为在poolChunkList中会根据poolChunk使用率将其分配到不同的poolChunkList中;
3.2.2.4.3.2)allocateSubpage
申请规格小于一页8kb,调用此方法;
3.2.2.4.3.3)initBuf
3.2.2.4.4)PoolChunkList#add
将新申请的poolChunk加入进合适的list中;

3.2.3)normCapacity <= chunkSize

说明normCapacity虽然大于maxSmallSize 4096,但是 小于 16mb;
3.2.3.1)allocateNormal方法
该方法和3.2.2.4)是同一个方法;

3.2.4)allocateHuge

说明 normCapacity 非常大,是大于16mb的!这个规格称为 huge,需要走特殊的分配逻辑

4)toLeakAwareBuffer方法

资源泄露相关的处理逻辑;

以上是流程总览;

allocateNode(d)方法的细节:

首先从根节点开始,每一层的最左节点分配能力值与目标深度值比较,若分配能力值小于目标深度值,则继续看下一层的最左节点,若不小于,则当前深度是要找到深度,接着再找这一层的具体哪个id,比较当前最左节点的分配能力值是否大于目标深度值,若大于则表示该节点不够分配,看其右边节点;

该方法有一个while循环,while (val < d || (id & initial) == 0),val表示节点分配能力值,d代表深度值,(id & initial) == 0表示当前id是不在深度为d的层;
所谓合适的节点,指分配能力值合适且其下没有子节点更合适,分配能力值合适比如申请1mb不能一下子给8mb,而分配能力值应该恰好为1mb;而其下没有子节点更合适意味着当发现某个父节点分配能力为1mb,但其实父节点原本分配能力值为2mb,但其左子节点已被完全分配出去,而右子节点才是真正分配1mb的节点,所以此时该右子节点才是要找的;
该方法的逻辑是先判断根节点是否够分配,若不够分配,则直接返回;若够分配,则再检查下一层的最左节点是否是更为合适的一个节点,此时有三种情况:
1)第一种情况是会一直找到深度d所在的层且最左节点就是合适节点;如需要找一个8kb,最终找到了深度为11的最左节点为合适节点;(val < d且其左子节点的val也小于d)
2)第二种情况是在小于深度d的层的节点,发现该节点已被分配出去了部分,此时需要找到其哪一个子节点可用;如需要找一个8kb,发现16kb已被分出去部分,此时还需要进一步找到可分配的子节点进行判断;(val不小于 d但(id & initial)为0)
3)第三种情况是在小于深度d的层的节点,并没有发现该节点已被分配出去了,实际上该节点已被分配出去了部分,只是没有进一步影响到分配能力值,所以此时在判断其子节点时,突然发现左子节点已不够分配了,则此时跳到右子节点;如需要找一个8kb,发现32kb的左子节点不够分配了,则找其右子节点进一步判断;(val < d但其左子节点的val不小于d)
当找到所需节点后,最后循环更新父节点的分配能力值,去子节点中较小值,因为较小值代表着更大的分配能力;

handle = allocateSubpage(normCapacity)方法细节
申请的内存属于tiny类型,则会将叶节点按照16byte、32byte、48byte…、496byte中的一种进行划分。比如申请10byte的内存,则会按照16byte均等地划分8K节点并返回16byte给调用者。
申请的内存属于small类型,则会按照512byte、1024byte、2048byte和4096byte中的一种划分。
申请小于8kb的内存,会进入此方法,此时会在poolChunk的叶子节点分配,PoolChunk会将叶子节点进行划分,划分的方式与申请的内存大小有关,对应tiny和small类型的内存,并不是按照2的n次幂进行申请,而是按照上述若干固定的大小进行分配。比如申请9byte,实际会申请16byte;申请40byte,实际会申请48byte;最后则会按照16byte和48byte均等地划分8K节点并返回给调用者。
在申请normal类型的内存时,使用了memoryMap记录节点的层数位置等信息;而在申请tiny或small时,均分的page使用了一个bitMap记录分配的位置;比如申请10byte,则会促使一个叶子节点按照16byte进行划分,总共划分了 8K / 16byte = 512个,则bitMap = new long[512 / Long.SIZE] = new long[8];

1)首先执行int id = allocateNode(d)方法找到合适的叶子节点;
剩下部分和下边分析的一致;

内存池回收

PooledByteBuf#deallocate方法;
1)设置handle为-1,memory为null(之前是保存jdk的16mb的byteBuffer),PoolChunk实例为null;
2)chunk.arena.free方法
释放逻辑首选方案是将内存缓存到cache,以便 cache归属线程,后备之需;如果cache满了,装不了这么多内存了,就将内存归还到chunk。
回收逻辑是根据handle,判断归还的是tiny,small类型的内存还是normal类型的;如果是tiny,small类型的内存则需要还原bitmap上的占用位为0,记录当前归还位置,下次申请可以直接定位到这里;如果归还的是normal类型,则更新poolChunk中剩余空闲内存大小,更新节点分配能力值为深度值,恢复父节点的能力值;

3)recycle()方法
PooledByteBuf 对象 归还到 “对象池”

资源泄漏监控

在newDirectBuffer方法的最后会执行toLeakAwareBuffer方法,该方法用于对byteBuf进行包装;有几种检测级别, 默认是simple级别,即进行抽样检测,其他三种分别是disable,advanced,paranoid。advanced也是抽样检测但追踪更详细,paranoid是对所有byteBuf都检测并且详细追踪,当为disable时,不对byteBuf进行包装;ResourceLeakDetector类用于检测资源,当实例化该类时,传入什么类型的T,就监测什么类型,这里传入的是ByteBuf.class;

toLeakAwareBuffer方法步骤如下:

1)如果是simple检测级别

1.1)先执行ResourceLeakDetector#track方法
1.1.1)若检测级别为disabled则返回null;
1.1.2)若检测级别是simple则进行采样,随机数采样,默认是128个byteBuf会追踪一个,若采样失败,则返回null,否则执行以下两步
1.1.2.1)reportLeak方法;
1.1.2.2)此时会调用DefaultResourceLeak的构造方法返回实例,DefaultResourceLeak继承了WeakReference,构造方法中关联了bytebuf,ReferenceQueue实例,并且将当前DefaultResourceLeak实例加入到了set集合中;
1.1.3)若检测级别是advanced或paranoid,则直接执行1.1.2.1)和1.1.2.2)步骤;
1.2)若1.1返回值不为null,则返回一个SimpleLeakAwareByteBuf实例,封装了当前byteBuf和1.1返回的DefaultResourceLeak实例;若1.1返回null,则此时直接返回byteBuf;

2)如果是advanced或paranoid检测级别

则步骤和simple一样,就是返回的是AdvancedLeakAwareByteBuf实例,封装也一样;若1.1返回null,则此时直接返回byteBuf;

当释放资源时,会调用SimpleLeakAwareByteBuf#release方法

1)若super.release方法返回true

则执行closeLeak方法;该方法 中会执行leak.close方法,会将当前DefaultResourceLeak实例从set集合中移除,以及断开与byteBuf的关联;

2)若super.release方法返回false

则直接返回false;
所以super.release方法就成为了关键,追溯源码可以发现,该方法调用路径是WrappedByteBuf#release方法——>AbstractReferenceCountedByteBuf#release方法,而该方法逻辑是引用计数减1后变为0了,就执行内存释放,返回true;引用计数减1后不为0则不执行内存释放,返回fasle;
所以当内存没有释放时,不会执行leak.close方法,意味着bytebuf一直被弱引用defaultResourceLeak关联,此时bytebuf被gc回收后,defaultResourceLeak会被加入到引用队列中,当下次再次执行toLeakAwareBuffer方法时,会执行reportLeak方法,取出引用队列中的元素,若存在于set集合中,则可认定super.release返回了false导致没有执行leak.close方法,归根结底是引用计数减1没有为0,所以内存也没有归还至池中;

思想

释放内存,释放成功,则断开内存与weakReference的关联,gc的时候,就不会将WeakReference实例加进队列中;释放失败,则内存与WeakReference之间依旧保持关联,gc时,就会将weakReference实例加入到队列中,程序再次分配新内存时,会检测到队列中有元素,则可以推断有内存释放失败;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

orcharddd_real

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值