随笔分类
对 Netty的些许思考:
内存池为什么会引入 Arena?Arena又解决了什么问题?
开辟多个 Arena,让尽可能少的线程去共享同一个 Arena,那么这也就能够去缓解在同一块内存上操作的带来的无法并行需要进行同步的问题
这怎么理解?
现代 cpu多核心,当实际去读取数据时,cpu并不会只去读你想要的数据,出于缓存 (局部性原理,L1,L2,L3),别的数据也会被读取
假设 core1、core2上分别运行着 thread-1,thread-2,而这两个线程分别对应的 ByteBuf 16b数据的读取,那么 cpu便会去进行数据的读取
此时,core1、core2对应的 L1、L2上会存在彼此的数据,而 L3上也存在(L3并不是每个 core都各自对应着了),此时 core存在相同数据了,即多线程可能会去操作同一块内存,此时,core便不能再去并行操作了,需要进行数据的同步操作,这是不可避免的
so,引入了 Arena
对 Netty中内存分配时涉及到的 "伙伴算法" 的剖析
Netty在分配内存时使用到了伙伴算法,那什么是伙伴算法?
Buddy算法,是计算机算法的一种,是为了核心内存管理能够快速响应请求,尽可能地提高内存利用率的同时,也去减少内存碎片
而对于 Netty使用的伙伴算法,怎么来去理解呢?
其实就是在分配内存时,若是当前节点无法进行内存的分配,就到其 "兄弟节点"上去进行内存的分配,这就实现了内存快速分配的定位,也实现了页内碎片的减少化
废话少说,直接上 Netty涉及到这块的源码:(到二叉树上去进行内存的分配)
// 条件一:val < d, val表示的是二叉树中某个节点的可分配深度能力值, 条件成立说明当前节点管理的内存比较大, 总之大于申请容量的,
// 需要到当前节点的下一级尝试申请
// 条件二(此时 val >= d, 这里主要来判断正好能够完全分配的情况 - 找到对应深度的对应节点):对于 id对应的节点在深度为 d时, 此时 id & initial == 1;
// 对于 < d而言, 此时会是 0, 即条件成立
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
// 到下一级去查找节点(靠左节点)
// id == 1, -> 2
id <<= 1;
// 获取 id对应节点的可分配深度能力值
val = value(id);
// 条件成立 - (当前节点已经分配内存出去了, 不够分配啦)接着便是来去获取兄弟节点了(对于每个节点, 只会有两个子节点)
if (val > d) {
// 伙伴算法, 来去寻找兄弟节点
// 如:2048 - 0b 0000 0000 0000 0000 0000 1000 0000 0000
// ^ 0b 0000 0000 0000 0000 0000 0000 0000 0001
// = 0b 0000 0000 0000 0000 0000 1000 0000 0001
id ^= 1;
// 获取兄弟节点的可分配深度能力值
val = value(id);
}
} // 出循环, 表示已经找到了可以去分配内存的节点 id
解释下几个变量,d是上层业务指定分配的内存容量大小在二叉树中对应的节点深度值,val初始值便是二叉树根节点对应的可分配深度能力值,即 0
而 initial则是一个关键的变量,对应的便是 - (1 << d)
,即根据 d来去计算出的一个值
我们知道,chunk默认管理着 16mb的内存,内部会去使用一个满二叉树去表示着 16mb内存的使用情况,二叉树默认深度为 11 (0 ~ 11),而对于二叉树中每个节点,都会有着三层维度的属性,即节点 id (从 1开始),节点深度,节点对应的可分配深度能力值
对于后两者,其实是使用数组来进行表示的,前者 memory[id],后者 depth[id]
对于节点的可分配深度能力值而言,该值是越小代表着其可去管理分配的空闲内存就越多,如当上层要求去分配的深度能力值为 4,而当前迭代的节点可分配深度能力值为 3时,那就说明当前迭代的节点管理的内存足够去进行分配,so,此时便可以去下一层其管理的节点去进行内存的分配了,对应的可分配深度能力值便是 4,这时伙伴算法就用到了
那么,正式开始分析吧!
仍然是使用 "代入法"进行分析,主逻辑分析:
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
// 到下一级去查找节点(靠左节点)
// id == 1, -> 2
id <<= 1;
// 获取 id对应节点的可分配深度能力值
val = value(id);
// 条件成立 - (当前节点已经分配内存出去了, 不够分配啦)接着便是来去获取兄弟节点了(对于每个节点, 只会有两个子节点)
if (val > d) {
// 伙伴算法, 来去寻找兄弟节点
// 如:2048 - 0b 0000 0000 0000 0000 0000 1000 0000 0000
// ^ 0b 0000 0000 0000 0000 0000 0000 0000 0001
// = 0b 0000 0000 0000 0000 0000 1000 0000 0001
id ^= 1;
// 获取兄弟节点的可分配深度能力值
val = value(id);
}
} // 出循环, 表示已经找到了可以去分配内存的节点 id
我们假设,此时的 val是 1,id是 1,此时 d是 11,计算出的 initilal是 0b 1111 1111 1111 1111 1111 1000 0000 0000
此时 chunk内存分配的情况:假设叶子节点 id为 2048的已经分配出去了,对应的便是 11层,此时节点 id为 1024对应的可分配深度能力值变为了 12,512 -> 10,256 -> 9,128 -> 8,64 -> 7,32 -> 6,16 -> 5,8 -> 4,4 -> 3,2 -> 2,1 -> 1
可知,当前根节点可分配的内存就变为了 8mb
so,此时 val < d,即 1 < 11,进入循环
此时,id更新为 2,val更新为了 2,下面 if不成立
由这里可以看出,id的变化对应的便是使用的便是深度优先的一个搜索算法,并且其靠左搜寻的一个算法
下一轮循环,2 < 11,id更新为了 4,val更新为了 3,下面 if不成立
下一轮循环,3 < 11,id更新为了 8,val更新为了 4,下面 if不成立
下一轮循环,4 < 11,id更新为了 16,val更新为了 5,下面 if不成立
下一轮循环,5 < 11,id更新为了 32,val更新为了 6,下面 if不成立
下一轮循环,6 < 11,id更新为了 64,val更新为了 7,下面 if不成立
下一轮循环,7 < 11,id更新为了 128,val更新为了 8,下面 if不成立
下一轮循环,8 < 11,id更新为了 256,val更新为了 9,下面 if不成立
下一轮循环,9 < 11,id更新为了 512,val更新为了 10,下面 if不成立
下一轮循环,10 < 11,id更新为了 1028,val更新为了 11,下面 if不成立
下一轮循环,11 < 11,while判断中条件一不成立,此时去判断条件二,此时 initial就起作用了
id & initial == 0 ?
此时 id为 1028,对应:
0b 0000 0000 0000 0000 0000 0100 0000 0100
&
0b 1111 1111 1111 1111 1111 1000 0000 0000
得 0
此时,可以进入循环:
此时,id为 2048,val更新为 12,此时 由于 12 > 11,进入到 if语句中
由前述,我们知道,此时 id为 2048的节点已经分配出去了,so,此节点不能被分配出去
此时便体现出了伙伴算法了,核心 id ^= 1
- 0b 0000 0000 0000 0000 0000 1000 0000 0000
0b 0000 0000 0000 0000 0000 0000 0000 0001
= 0b 0000 0000 0000 0000 0000 1000 0000 0001
此时,id更新为 2049,这不正是 id为 2048的节点的相邻节点吗!
这便是,Buddy算法!
so,解读完毕!