第一部分提到了ByteBuf的内存结构,这儿有几点需要补充并在某些点展开说明
大对象的处理
当申请的分配对象超过Chunk容量大小,Netty就不在使用池化管理的方式了,在每次请求分配内存时单独创建非池化PoolChunk对象进行管理,当对象释放时整个PoolChunk内存释放。
小对象处理
如果申请的内存空间远小于PageSize的话,按照之前提到的,就需要为每个小ByteBuf对象分配一个Page,这就出现内存浪费和内存碎片的问题,Netty将再把Page细分,Netty 将请求的空间大小向上取最近的 16 的倍数(或 2 的幂),规整后小于 PageSize 的小 Buffer 可分为两类。
- 微型对象:规整后的大小为 16 的整倍数,如 16、32、48、......、496,一共 31 种大小。
- 小型对象:规整后的大小为 2 的幂,如 512、1024、2048、4096,一共 4 种大小。
这样的话,当向系统申请小对象存储空间时,Netty会先从Poolchunk中申请空闲的Page,同一个Page分为相同大小的buffer,这些Page用PoolSubpage对象进行封装,PoolSubpage内部会记录它自己能分配的小buffer的的规格,可用内存数量,并通过bitmap的方式记录各个小内存的使用情况。虽然这种方案不能完美消灭内存碎片,但是却在很大程度上减少了内存浪费。
基于二叉平衡树的算法结构
上个章节提到Netty中的池化技术实现是基于分层的结构,具体到数据结构的话就是用二叉平衡树进行的实现 当需要创建一个给定大小的ByteBuf,算法需要在PoolChunk中大小为chunkSize的内存中,找到第一个能够容纳申请分配内存的位置
为了方便快速查找chunk中能容纳请求内存的位置,算法构建一个基于byte数组(memoryMap)存储的完全平衡树,该平衡树的多个层级深度,就是前面介绍的按照不同粒度对chunk进行多层分组:
当申请分配大小为chunkSize/2^k的内存,在平衡树高度为k的层级中,从左到右搜索第一个空闲节点
数组的使用域从index = 1开始,将平衡树按照层次顺序依次存储在数组中,depth = n的第1个节点保存在memoryMap[2^n] 中,第2个节点保存在memoryMap[2^n+1]中,以此类推(下图代表已分配chunkSize/2)
可以根据memoryMap[id]的值得出节点的使用情况,memoryMap[id]值越大,剩余的可用内存越少
- memoryMap[id] = depth_of_id:id节点空闲, 初始状态,depth_of_id的值代表id节点在树中的深度
- memoryMap[id] = maxOrder + 1:id节点全部已使用,节点内存已完全分配,没有一个子节点空闲
- depth_of_id < memoryMap[id] < maxOrder + 1:id节点部分已使用 ,memoryMap[id] 的值 x,代表id的子节点中,第一个空闲节点位于深度x,在深度[depth_of_id, x)的范围内没有任何空闲节点 (引用链接:juejin.cn/post/684490...