Netty 篇之内存管理器
一. 概述
在计算机组成结构中,内存是存储器中充当了很关键的一个角色。那么内存管理是由操作系统管理的,进程每次使用内存时,都要向操作系统申请内存,这个时候会进行一次上下文切换;如果进程频繁的调用操作系统的内存分配等内存操作 api 时,是非常低效的;所以很多开源的系统,都会在内存管理这个模块中花大量的心思去提高性能,也就衍生出应用系统的内存管理器。如 redis 使用的内存管理器 ltcmalloc、ltcmalloc_minimal
这篇主要介绍的 netty 的内存管理器,其使用的是 jemalloc 内存管理器;而这里只是介绍推外内存管理;
二. 内存管理器概述
在应用系统使用内存时,需要向操作系统提交申请内存指令;不在使用时,会向操作系统提交内存释放指令,如下图:
应用系统调用操作系统提供的方法时,都会进行一次上下文切换,性能会有所低效;
所以,一般内存管理器都会一次性向操作系统申请较大的内存区域,由业务系统自己去管理内存,这时候内存分配以及释放内存都不会调用操作系统,如下图:
然而上图的内存管理器存在性能低效问题,主要是由于并发场景导致的;为了解决并发的进行内存分配,添加了线程分配特性,减少并发场景发生,此时线程对象又是一个小型的内存管理器,如下图:
上面的流程看样子是比较简单的,然而里面的逻辑是比较复杂的;如何判断一个内存管理器是好的,一般从几个方面去衡量:
内存使用率,一般会从碎片比例来看出;
并发场景发生频率,
上下文切换次数,即调用操作系统所提供的内存操作 api。我们可以一次性申请固定大小的内存;
在 java 语言中,我们往往会创建一个对象来封装对 ByteBuffer 的操作;每次申请内存时,都要创建这么一个对象去操作内存;为了减少对象的创建,添加了对象池特性,如图:
三. JEMalloc 内存管理器
在介绍其原理时,先介绍几个概念:PoolArena、PoolChunk 、 PoolSubpage。这三个都是对一块内存管理;然而其区别主要的内存区域大小而已;下面的图可以更加直观看到他们之间的关系;该图是借鉴网友所画的图;具有一定的参考价值;原因是高版本是不再由 tinysubpages 管理;
PoolSubpage 没有在上面的图中,主要的原因是该类的结构相对较简单,同时也不影响整体的理解:PoolArena 管理 PoolChunk,PoolChunk 管理 PoolSubpage;
JEMalloc 管理的内存,有三种类型,
内存<= 28672K,这个是由 PoolSubpage 进行管理;
28672K< 内存 <=16M(chunk 默认大小),则由 PoolChunk 进行管理;
内存>16M,则不在其管理范围内,直接访问操作系统申请内存;
这里画个简单图来阐述其交互过程:
大于 4096 同时小于 16M(chunk 默认大小)的内存分配,那不再有 PoolSubpage 参与了;
而内存释放呢,则是上面流程图的方向操作,这里不再提供了;
这里稍微留意的是,PoolChunk 有合并操作,即是对一些被释放内存尝试做合并,以便下次提供内存分配时,提供内存的使用率;
具体的细节将在 netty 中的 Jemalloc 章节进一步介绍;
四. Netty 之 Jemalloc
上面简单介绍了 jemalloc 的一些概念以及简单的流程图,接下来将参照代码来详细的介绍其运作过程;其代码是 netty 仓库中的 4.1 分支进行解读;
4.1 SizeClasss
这个抽象类很大程度上介绍了内存管理的大部分细节,这里含盖了很多计算,公式目前我还没吃透,直接通过 debug 的形式,来拿到对应目标值;而且网上也有很好的文章对其进行了介绍;这里不再班门弄斧,在这里只是做个总结;
log2Group: 组中的 2 的指数
log2Delta: 增量中的 2 的指数
nDelta: 数学公式中的【δ】用来表示增量值
isMultiPageSize: 是否是页的倍数,一页=8192
isSubPage: 是否是 small 内存,指的小于 28672K
log2DeltaLookup: Same as log2Delta if a lookup table size class, 'no' otherwise.
nSubpages: small 的数量,默认值为.39
nSizes: size 的数量,值为 76 个
nPSizes: 页的倍数有多少个,指为 40 个
smallMaxSizeIdx: 小内存(小于 28672K)最大的角标值 38
lookupMaxClass: 指 Log2DeltaLookup 中值不为 0 的最大角标
而 PageIdx2sizeTab 是提供给 chunk 使用的,也就是 chunk 能分配的内存大小;例如 chunk 要分配 172032 大小的内存,那么就会分配 196608 大小的内存,而不是只分配 172032 大小的内存;
4.2 PoolArena
是用来管理 PoolChunk 对象的;当需要分配内存时,需要一个机制来选择对应的 PoolChunk 进行分配;在该类中有 q***开头的属性变量,维护了不同使用率的 Chunk 列表;
还有额外的 PoolSubpage 数组
所以说,PoolArena 是一个壳,其不提供具体的内存分配等工作,而是交给 PoolChunk 和 PoolSubpage 进行内存分配;它只负责提供工作分配,从而提高内存使用率;
4.3 PoolChunk
是用来管理 PoolSubpage 对象以及 Normal 内存块的;
在介绍 PoolChunk 之前,先介绍 handle 的使用;其长度为 64 位,用来标志内存的使用情况;
runOffset 用来表示 chunk 内存快中的页的偏移量
size 用来表示使用多少页的内存块
isUsed 用来表示该内存块是否被暂用
isSubpage 用来表示是否是 small 内存块
bitmap 用来表示 subpage 分配的角标位置
具体场景来说明:
PoolChunk 对象中维护了优先队列数组,来对 chunk 内存进行管理,如下图所示
同时也维护了 subpages 数组,其与 PoolArena 的存放形式不一样,并没形成链表;
其数组大小为 2048,刚好对应 chunk 所能分配的页数量大小;
其角标是记录的 small 内存中 subpage 的内存块对 chunk 的页偏移量;例子如下:
4.4 PoolSubpage
其是对 small 内存块进行管理的;当分配 2048 内存块时,会优先分配 8192 大小的内存块,给到 Poolsubpage 对象中,那就意味着这个对象就可以分配 4 个 2048 内存操作;
其属性有很一些关键成员,介绍一下;
elemSize 每个成员所占用的内存大小,在上面的例子中,elemSize = 2048
bitmap 是一个 long 数组,每一个 long 的每一位来标志是否已经分配了,数组长度是根据 runsize 来计算,数组长度= runsize >> 6 >> 4; 6 代表的是 long 类型的长度 2^6=64,而 4 代表的内存最小单位为 2^4=16,下面举个例子来介绍其原理;
4.5 线程缓存
netty 内存分配时,每次都会经过 PoolThreadCache,每个内存分配都尝试去线程缓存中去获取对应的内存块;这块逻辑相对于比较简单,不再阐述;
这里稍微说的是配置项;
useCacheForAllThreads 是否使用线程缓存
smallCacheSize small 内存块的缓存数量大小
normalCacheSize normal 内存块的缓存数量大小
只有设置了这些值,才可以使用线程缓存特性,提高内存分配效率;
4.6 ObjectPool
对象池,每次分配内存时都需要一个对象去承载这些内存的使用,为了减少对象的创建以及销毁,netty 增加了对象池的特性;具体的实现类为 RecyclerObjectPool 以及 Recycler 抽象类;这里不再阐述了;
等需要使用的场景时,再进一步解读里面的源码;
4.7 总结
上面的篇幅,主要是介绍了内存的分配的工作,以及其他的额外的特性;对内存管理有了一定程度的认识;
里面的内存释放,涉及到的操作细节非常多,例如内存合并操作;以及内存真正释放的时机;没有进行阐述,可以通过看代码了解;
这里列一下关键的 netty 中的几个类:
ServerChannelRecvByteBufAllocator 分配缓存大小的策略对象
PooledByteBufAllocator 字节缓存池分配器
PoolThreadCache 线程缓存对象
如果基于 netty 的优化方面,可以释放的往内存分配去想,
例如内存颗粒度的评估,这样子可以有效的减少内存碎片化问题
线程缓存大小,可以提供内存分配效率
引用
这里有比较好的文章,这里引用一下:
版权声明: 本文为 InfoQ 作者【邱学喆】的原创文章。
原文链接:【http://xie.infoq.cn/article/f1d68f0a6ab1f97998a95691d】。文章转载请联系作者。
评论