最近项目中有使用到jemalloc
作为内存分配器,但是使用后发现应用所占用的内存在不断飙高,而且无下降的趋势。经过阅读源码和进行实验逐步发现jemalloc
隐藏的使用风险——脏页释放规则。
jemalloc简介
jemalloc
是一款很优秀的内存分配器,按照他们wiki的说法,已经被Redis
、Android
等多个大型项目采用。从jemalloc
在github上的提交记录来看,jemalloc
已经由facebook公司的员工在做维护更新工作。
jemalloc实现
jemalloc实现这里不多赘述,5.1.0版本及之后的实现参见jemalloc,作者在这篇文章中做了详细的说明。回到本文最初提到的脏页释放规则带来的风险,该风险的引入就与其中的jemalloc脏页释放规则有关。
内存释放规则及风险
jemalloc在4.5.0以及这个版本之前,脏页的释放条件是根据活动页页数:脏页页数决定的,默认配置下当其比例小于8时jemalloc将会启用脏页回收机制。但这种机制在实际项目中也会存在尴尬的境地,如果一个应用的活动页与脏页的比例恰好在8:1附近波动,这将会导致jemalloc频繁释放脏页和从系统申请新内存,出现过多的系统调用,这对于性能敏感的应用就是一场灾难。
为了避免这种情况的发生,4.5.0之后的版本jemalloc更改了脏页释放条件。脏页的释放不由脏页的数量决定,而由用户申请、释放内存的频率决定。具体的实现如下。
jemalloc为每一个内存区域arena
设置一个tick
数统计,tick
数的初始值为1000,对该arena
的每一次内存申请、释放这个tick
统计值将会减1,直到减至0后jemalloc会对该arena
进行脏页的整理和回收。这次改动看似很好的解决了之前版本脏页释放规则带来的问题,但是如果一个应用线程在启动后申请了大量的内存释放,之后再也不进行任何内存申请、释放操作,此时的tick
如果没有达到0,那个之前申请后释放的内存将会一直标记为脏页,直到线程退出后才会释放。
解决方案
为了规避上述风险,可以采取如下方案
- 在编译时开启后台回收线程选项
通过设置background_thread:true
可以使jemalloc在后台回收脏页。 - 将脏页释放的时间设置为0
此处的时间为脏页释放的时间,并非是上面所提到的tick
。脏页释放的时间是脏页开始释放到释放完毕所花费的时间,脏页的释放并非是一次性释放完毕的,为了避免内存断崖式的大量释放,jemalloc采用平滑的方式释放脏页,而在多长时间内释放完毕则由dirty_decay_ms
决定。
在将dirty_decay_ms
设置为0后,jemalloc脏页释放的规则将发生改变,当上级slab
链表完全为空后,jemalloc将会对脏页extent进行规整然后进行释放,不再受到tick
数的约束。当然这样做的坏处也是显而易见的,脏页释放的频率会因此变高,对于那些频繁申请释放内存的程序将会增加mmap
与munmap
的调用频率。
5.1.0版本的bug
而在使用5.1.0版本的jemalloc同时存在另外一个严重的问题,那就是内存泄漏问题。当一个线程还没有申请内存之前将其它线程申请的内存给释放之后,接下来这个线程所有申请与释放内存都会标记为脏页不再释放。问题描述可以参见Maybe a terrible bug in the multi-threaded usage scenario。
解决方案同上。