Redis的内存管理是由zmalloc.h和zmalloc.c实现的,编译时会先判断是否使用tcmalloc(google开发的内存分配器,全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理)和jemalloc(一种内存分配器,与其它内存分配器相比,它最大的优势在于多线程情况下的高性能以及内存碎片的减少),然后判断是否为Mac系统,如果上面三者都无法使用的话才会使用标准库libc。默认情况下Redis6.0使用的是jemalloc,关于jemalloc, 这个博文讲的很好。

zmalloc.h重要代码

图片说明
选择的内存管理的方式不同,相应的zmalloc_size(p)这个宏(用来获得实际从堆中分配的内存字节数)也有不同的定义。

zmalloc.c的重要代码

  • 首先需要注意的是根据选择的内存管理器,会将malloc、free等函数"重写"为管理器的实现版本。比如Redis默认使用的是jemalloc,就会执行接下来这一段宏定义:

    #define malloc(size) je_malloc(size)
    #define calloc(count,size) je_calloc(count,size)
    #define realloc(ptr,size) je_realloc(ptr,size)
    #define free(ptr) je_free(ptr)
    #define mallocx(size,flags) je_mallocx(size,flags)
    #define dallocx(ptr,flags) je_dallocx(ptr,flags)
  • 从zmalloc函数开始,实现代码为:

    void *zmalloc(size_t size) {
     void *ptr = malloc(size+PREFIX_SIZE);
    
     if (!ptr) zmalloc_oom_handler(size);
    #ifdef HAVE_MALLOC_SIZE
     update_zmalloc_stat_alloc(zmalloc_size(ptr));
     return ptr;
    #else
     *((size_t*)ptr) = size;
     update_zmalloc_stat_alloc(size+PREFIX_SIZE);
     return (char*)ptr+PREFIX_SIZE;
    #endif
    }

    PREFIX_SIZE的大小如果定义了HAVE_MALLOC_SIZE为0,否则大小为sizeof(size_t)或sizeof(long long)。当内存分配失败时调用zmalloc_oom_handler函数,默认实现就是打印错误信息并终止程序,和我们平常对内存分配失败的处理方式类似。
    重头戏是接下来的几行代码,如果定义了HAVE_MALLOC_SIZE(注意在Redis3.x版本里如果使用的是libc,是不会定义HAVE_MALLOC_SIZE的!zmalloc_size也需要在zmalloc.c中定义。而在6.0版本里如果使用libc是会定义HAVE_MALLOC_SIZE的,并且也提供了malloc_usable_size函数作为zmalloc_size的实现!估计这也是标准库不断进化的结果吧),执行updata_zmalloc_stat_alloc用来更新Redis已使用的内存字节数(used_memory),传入的参数是实际从堆中分配的内存字节数(使用之前提到的zmalloc_size)。
    updata_zmalloc_stat_alloc的定义如下:

    #define update_zmalloc_stat_alloc(__n) do { \
     size_t _n = (__n); \
     if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
     atomicIncr(used_memory,__n); \
    } while(0)

    因为在64位Linux系统中,sizeof(long)=8,所以if语句可以简化为: if(_n&7) _n += 8 - _n&7;
    这段代码主要是判断分配的内存空间大小是否为8的倍数(使用位运算而不是_n%8,效率更高),不是的话就加上相应的偏移量使之变成8的倍数。因为malloc分配的内存都是8字节对齐的,所以如果你要分配的内存不是8的倍数,malloc就会多分配一点来凑成8的倍数,这样使得used_memory能够正确的维护已分配内存的大小。这里使用的atomicIncr是在atomicvar.h中定义的,确保变量的增、减操作是线程安全的,实现原理为如果系统提供了原子(atomic)或同步(sync)的机制,使用之,否则就使用互斥锁。
    没有定义HAVE_MALLOC_SIZE的情况,简单的说就是多分配PREFIX_SIZE的空间,并在已分配的内存头部存放上层申请的内存大小(即参数size),再进行内存对齐。在我看来,这一段代码其实已经没有太多的用武之地了,因为即使使用的是libc库现在也已经定义了HAVE_MALLOC_SIZE。
    zcalloc和zrealloc的实现比较简单,代码如下:

    void *zcalloc(size_t size) {
     void *ptr = calloc(1, size+PREFIX_SIZE);
    
     if (!ptr) zmalloc_oom_handler(size);
    #ifdef HAVE_MALLOC_SIZE
     update_zmalloc_stat_alloc(zmalloc_size(ptr));
     return ptr;
    #else
     *((size_t*)ptr) = size;
     update_zmalloc_stat_alloc(size+PREFIX_SIZE);
     return (char*)ptr+PREFIX_SIZE;
    #endif
    }
    void *zrealloc(void *ptr, size_t size) {
    #ifndef HAVE_MALLOC_SIZE
     void *realptr;
    #endif
     size_t oldsize;
     void *newptr;
    
     if (size == 0 && ptr != NULL) {
         zfree(ptr);
         return NULL;
     }
     if (ptr == NULL) return zmalloc(size);
    #ifdef HAVE_MALLOC_SIZE
     oldsize = zmalloc_size(ptr);
     newptr = realloc(ptr,size);
     if (!newptr) zmalloc_oom_handler(size);
    
     update_zmalloc_stat_free(oldsize);
     update_zmalloc_stat_alloc(zmalloc_size(newptr));
     return newptr;
    #else
     realptr = (char*)ptr-PREFIX_SIZE;
     oldsize = *((size_t*)realptr);
     newptr = realloc(realptr,size+PREFIX_SIZE);
     if (!newptr) zmalloc_oom_handler(size);
    
     *((size_t*)newptr) = size;
     update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
     update_zmalloc_stat_alloc(size+PREFIX_SIZE);
     return (char*)newptr+PREFIX_SIZE;
    #endif
    }
  • 再看zfree函数的实现:

    void zfree(void *ptr) {
    #ifndef HAVE_MALLOC_SIZE
     void *realptr;
     size_t oldsize;
    #endif
    
     if (ptr == NULL) return;
    #ifdef HAVE_MALLOC_SIZE
     update_zmalloc_stat_free(zmalloc_size(ptr));
     free(ptr);
    #else
     realptr = (char*)ptr-PREFIX_SIZE;
     oldsize = *((size_t*)realptr);
     update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
     free(realptr);
    #endif
    }

    对于定义了HAVE_MALLOC_SIZE,调用update_zmalloc_stat_free,也是用来更新已分配的内存字节数的,实现为:

    #define update_zmalloc_stat_free(__n) do { \
     size_t _n = (__n); \
     if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
     atomicDecr(used_memory,__n); \
    } while(0)

    其实也是8字节对齐后,再调用atomicDecr线程安全的减少已使用的内存字节数。

  • 关于内存分配和释放的内容大概就是这些,但在zmalloc.c中还实现了一些其它比较有用的函数。
    复制字符串函数zstrdup

    char *zstrdup(const char *s) {
     size_t l = strlen(s)+1;
     char *p = zmalloc(l);
    
     memcpy(p,s,l);
     return p;
    }

    获得已使用内存字节数zmalloc_used_memory

    size_t zmalloc_used_memory(void) {
     size_t um;
     atomicGet(used_memory,um);
     return um;
    }

    设置内存分配失败处理函数zmalloc_set_oom_handler

    void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
     zmalloc_oom_handler = oom_handler;
    }

总结

Redis可以使用4种内存分配器(tcmalloc、jemalloc、Mac、libc),默认使用jemalloc。
zmalloc、zfree除了申请、释放空间外,会将申请、释放的内存进行8字节对齐,再线程安全的更新已使用的内存字节数(used_memory)。