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中还实现了一些其它比较有用的函数。
复制字符串函数zstrdupchar *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)。