最近项目中有使用到jemalloc作为内存分配器,但是使用后发现应用所占用的内存在不断飙高,而且无下降的趋势。经过阅读源码和进行实验逐步发现jemalloc隐藏的使用风险——脏页释放规则。

jemalloc简介

jemalloc是一款很优秀的内存分配器,按照他们wiki的说法,已经被RedisAndroid等多个大型项目采用。从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,那个之前申请后释放的内存将会一直标记为脏页,直到线程退出后才会释放

解决方案

为了规避上述风险,可以采取如下方案

  1. 在编译时开启后台回收线程选项
    通过设置background_thread:true可以使jemalloc在后台回收脏页。
  2. 将脏页释放的时间设置为0
    此处的时间为脏页释放的时间,并非是上面所提到的tick。脏页释放的时间是脏页开始释放到释放完毕所花费的时间,脏页的释放并非是一次性释放完毕的,为了避免内存断崖式的大量释放,jemalloc采用平滑的方式释放脏页,而在多长时间内释放完毕则由dirty_decay_ms决定。
    在将dirty_decay_ms设置为0后,jemalloc脏页释放的规则将发生改变,当上级slab链表完全为空后,jemalloc将会对脏页extent进行规整然后进行释放,不再受到tick数的约束。当然这样做的坏处也是显而易见的,脏页释放的频率会因此变高,对于那些频繁申请释放内存的程序将会增加mmapmunmap的调用频率。

5.1.0版本的bug

而在使用5.1.0版本的jemalloc同时存在另外一个严重的问题,那就是内存泄漏问题。当一个线程还没有申请内存之前将其它线程申请的内存给释放之后,接下来这个线程所有申请与释放内存都会标记为脏页不再释放。问题描述可以参见Maybe a terrible bug in the multi-threaded usage scenario

解决方案同上。