因为计算机的内存由操做系统进行管理,因此普通应用程序是没法直接对内存进行访问的, 应用程序只能向操做系统申请内存,一般的应用也是这么作的,在须要的时候经过相似malloc之类的库函数 向操做系统申请内存,在一些对性能要求较高的应用场景下是须要频繁的使用和释放内存的, 好比Web服务器,编程语言等,因为向操做系统申请内存空间会引起系统调用, 系统调用和普通的应用层函数调用性能差异很是大,由于系统调用会将CPU从用户态切换到内核, 由于涉及到物理内存的操做,只有操做系统才能进行,而这种切换的成本是很是大的, 若是频繁的在内核态和用户态之间切换会产生性能问题。php
一些对性能有要求的应用一般会本身在用户态进行内存管理, 例如第一次申请稍大的内存留着备用,而使用完释放的内存并非立刻归还给操做系统, 能够将内存进行复用,这样能够避免屡次的内存申请和释放所带来的性能消耗。编程
PHP的内存管理分为三层:存储层(storage)、堆层(heap)和接口层(emalloc/efree)。 windows
存储层经过 malloc()、mmap() 等函数向系统真正的申请内存,并经过 free() 函数释放所申请的内存。 存储层一般申请的内存块都比较大,这里申请的内存大并非指storage层结构所须要的内存大, 而是堆层经过调用存储层的分配方法时,其以大块大块的方式申请的内存,存储层的做用是将内存分配的方式对堆层透明化。 如图所示, PHP在存储层共有4种内存分配方案: malloc,win32,mmap_anon,mmap_zero, 默认使用malloc分配内存,若是设置了ZEND_WIN32宏,则为windows版本,调用HeapAlloc分配内存, 剩下两种内存方案为匿名内存映射,而且PHP的内存方案能够经过设置环境变量来修改。api
接口层宏定义(位置:Zend/zend_alloc.h,php5.6.10)数组
/* Standard wrapper macros */ #define emalloc(size) _emalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define safe_emalloc(nmemb, size, offset) _safe_emalloc((nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define efree(ptr) _efree((ptr) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define ecalloc(nmemb, size) _ecalloc((nmemb), (size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define erealloc(ptr, size) _erealloc((ptr), (size), 0 ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define safe_erealloc(ptr, nmemb, size, offset) _safe_erealloc((ptr), (nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define erealloc_recoverable(ptr, size) _erealloc((ptr), (size), 1 ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define estrdup(s) _estrdup((s) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define estrndup(s, length) _estrndup((s), (length) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define zend_mem_block_size(ptr) _zend_mem_block_size((ptr) TSRMLS_CC ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) /* Relay wrapper macros */ #define emalloc_rel(size) _emalloc((size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define safe_emalloc_rel(nmemb, size, offset) _safe_emalloc((nmemb), (size), (offset) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define efree_rel(ptr) _efree((ptr) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define ecalloc_rel(nmemb, size) _ecalloc((nmemb), (size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define erealloc_rel(ptr, size) _erealloc((ptr), (size), 0 ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define erealloc_recoverable_rel(ptr, size) _erealloc((ptr), (size), 1 ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define safe_erealloc_rel(ptr, nmemb, size, offset) _safe_erealloc((ptr), (nmemb), (size), (offset) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define estrdup_rel(s) _estrdup((s) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define estrndup_rel(s, length) _estrndup((s), (length) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC) #define zend_mem_block_size_rel(ptr) _zend_mem_block_size((ptr) TSRMLS_CC ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_CC)
heap层代码以下缓存
/* mm block type */ typedef struct _zend_mm_block_info { #if ZEND_MM_COOKIES size_t _cookie; #endif size_t _size;//block大小 size_t _prev;//前一个块 } zend_mm_block_info; #if ZEND_DEBUG typedef struct _zend_mm_debug_info { const char *filename; uint lineno; const char *orig_filename; uint orig_lineno; size_t size; #if ZEND_MM_HEAP_PROTECTION unsigned int start_magic; #endif } zend_mm_debug_info; #elif ZEND_MM_HEAP_PROTECTION typedef struct _zend_mm_debug_info { size_t size; unsigned int start_magic; } zend_mm_debug_info; #endif typedef struct _zend_mm_block { zend_mm_block_info info; #if ZEND_DEBUG unsigned int magic; # ifdef ZTS THREAD_T thread_id; # endif zend_mm_debug_info debug; #elif ZEND_MM_HEAP_PROTECTION zend_mm_debug_info debug; #endif } zend_mm_block; typedef struct _zend_mm_small_free_block {//双向链表 zend_mm_block_info info; #if ZEND_DEBUG unsigned int magic; # ifdef ZTS THREAD_T thread_id; # endif #endif struct _zend_mm_free_block *prev_free_block;//前一个块 struct _zend_mm_free_block *next_free_block;//后一个块 } zend_mm_small_free_block;//小的空闲块 typedef struct _zend_mm_free_block {//双向链表+树结构 zend_mm_block_info info; #if ZEND_DEBUG unsigned int magic; # ifdef ZTS THREAD_T thread_id; # endif #endif struct _zend_mm_free_block *prev_free_block;//前一个块 struct _zend_mm_free_block *next_free_block;//下一个块 struct _zend_mm_free_block **parent;//父节点 struct _zend_mm_free_block *child[2];//子节点 } zend_mm_free_block; #define ZEND_MM_NUM_BUCKETS (sizeof(size_t) << 3) #define ZEND_MM_CACHE 1 #define ZEND_MM_CACHE_SIZE (ZEND_MM_NUM_BUCKETS * 4 * 1024) #ifndef ZEND_MM_CACHE_STAT # define ZEND_MM_CACHE_STAT 0 #endif struct _zend_mm_heap { int use_zend_alloc;//是否使用zend内存管理器 void *(*_malloc)(size_t);//内存分配函数 void (*_free)(void*);//内存释放函数 void *(*_realloc)(void*, size_t);//从新分配内存函数 size_t free_bitmap;//小块空闲内存标识 size_t large_free_bitmap;//大块内存标识 size_t block_size;//一次分配的块大小,即ZEND_MM_SEG_SIZE的大小,默认(256 * 1024) size_t compact_size;//压缩操做边界值,ZEND_MM_COMPACT,默认(2 * 1024 * 1024) zend_mm_segment *segments_list;//段指针列表 zend_mm_storage *storage;//所调用的存储层 size_t real_size;//堆的真实大小 size_t real_peak;//堆真实大小的峰值 size_t limit;//堆的内存边界 size_t size;//堆大小 size_t peak;//堆大小的峰值 size_t reserve_size;//备用堆大小 void *reserve;//备用堆指针 int overflow;//内存溢出数 int internal; #if ZEND_MM_CACHE unsigned int cached;//已缓存大小 zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS];//缓存数组 #endif zend_mm_free_block *free_buckets[ZEND_MM_NUM_BUCKETS*2];//小块内存数组,相似于索引 zend_mm_free_block *large_free_buckets[ZEND_MM_NUM_BUCKETS];//大块内存数组,相似于索引 zend_mm_free_block *rest_buckets[2];//剩余内存数组 int rest_count; #if ZEND_MM_CACHE_STAT struct { int count; int max_count; int hit; int miss; } cache_stat[ZEND_MM_NUM_BUCKETS+1]; #endif };
当初始化内存管理时,调用函数是zend_mm_startup。它会初始化storage层的分配方案, 初始化段大小,压缩边界值,并调用zend_mm_startup_ex()初始化堆层。 这里的分配方案就是图6.1所示的四种方案,它对应的环境变量名为:ZEND_MM_MEM_TYPE。 这里的初始化的段大小能够经过ZEND_MM_SEG_SIZE设置,若是没设置这个环境变量,程序中默认为256 * 1024。 这个值存储在_zend_mm_heap结构的block_size字段中,未来在维护的三个列表中都没有可用的内存中,会参考这个值的大小来申请内存的大小。服务器
PHP中的内存管理主要工做就是维护三个列表:小块内存列表(free_buckets)、 大块内存列表(large_free_buckets)和剩余内存列表(rest_buckets)。cookie
咱们能够把维护的前面两个表看做是两个HashTable,那么,每一个HashTable都会有本身的hash函数。 首先咱们来看free_buckets列表,这个列表用来存储小块的内存分配,其hash函数为:数据结构
#define ZEND_MM_BUCKET_INDEX(true_size) ((true_size>>ZEND_MM_ALIGNMENT_LOG2)-(ZEND_MM_ALIGNED_MIN_HEADER_SIZE>>ZEND_MM_ALIGNMENT_LOG2))
假设ZEND_MM_ALIGNMENT为8(若是没有特殊说明,本文的ZEND_MM_ALIGNMENT的值都为8),则ZEND_MM_ALIGNED_MIN_HEADER_SIZE=16, 若此时true_size=256,则((256>>3)-(16>>3))= 30。 当ZEND_MM_BUCKET_INDEX宏出现时,ZEND_MM_SMALL_SIZE宏通常也会同时出现, ZEND_MM_SMALL_SIZE宏的做用是判断所申请的内存大小是否为小块的内存, 在上面的示例中,小于272Byte的内存为小块内存,则index最多只能为31, 这样就保证了free_buckets不会出现数组溢出的状况。app
在内存管理初始化时,PHP内核对初始化free_buckets列表。 从heap的定义咱们可知free_buckets是一个数组指针,其存储的本质是指向zend_mm_free_block结构体的指针。 开始时这些指针都没有指向具体的元素,只是一个简单的指针空间。 free_buckets列表在实际使用过程当中只存储指针,这些指针以两个为一对(即数组从0开始,两个为一对),分别存储一个个双向链表的头尾指针。 其结构如图所示:
对于free_buckets列表位置的获取,关键在于ZEND_MM_SMALL_FREE_BUCKET宏,宏代码以下:
#define ZEND_MM_SMALL_FREE_BUCKET(heap, index) \ (zend_mm_free_block*) ((char*)&heap->free_buckets[index * 2] + \ sizeof(zend_mm_free_block*) * 2 - \ sizeof(zend_mm_small_free_block))
仔细看这个宏实现,发如今它的计算过程是取free_buckets列表的偶数位的内存地址加上 两个指针的内存大小并减去zend_mm_small_free_block结构所占空间的大小。 而zend_mm_free_block结构和zend_mm_small_free_block结构的差距在于两个指针。 据此计算过程可知,ZEND_MM_SMALL_FREE_BUCKET宏会获取free_buckets列表 index对应双向链表的第一个zend_mm_free_block的prev_free_block指向的位置。 free_buckets的计算仅仅与prev_free_block指针和next_free_block指针相关, 因此free_buckets列表也仅仅须要存储这两个指针。
那么,这个数组在最开始是怎样的呢? 在初始化函数zend_mm_init中free_buckets与large_free_buckts列表一块儿被初始化。 以下代码:
p = ZEND_MM_SMALL_FREE_BUCKET(heap, 0); for (i = 0; i < ZEND_MM_NUM_BUCKETS; i++) { p->next_free_block = p; p->prev_free_block = p; p = (zend_mm_free_block*)((char*)p + sizeof(zend_mm_free_block*) * 2); heap->large_free_buckets[i] = NULL; } heap->rest_buckets[0] = heap->rest_buckets[1] = ZEND_MM_REST_BUCKET(heap); heap->rest_count = 0;
对于free_buckets列表来讲,在循环中,偶数位的元素(索引从0开始)将其next_free_block和prev_free_block都指向本身, 以i=0为例,free_buckets的第一个元素(free_buckets[0])存储的是第二个元素(free_buckets[1])的地址, 第二个元素存储的是第一个元素的地址。 此时将可能会想一个问题,在整个free_buckets列表没有内容时,ZEND_MM_SMALL_FREE_BUCKET在获取第一个zend_mm_free_block时, 此zend_mm_free_block的next_free_block元素和prev_free_block元素却分别指向free_buckets[0]和free_buckets[1]。
在整个循环初始化过程当中都没有free_buckets数组的下标操做,它的移动是经过地址操做,以加两个sizeof(zend_mm_free_block*)实现, 这里的sizeof(zend_mm_free_block*)是获取指针的大小。好比如今是在下标为0的元素的位置, 加上两个指针的值后,指针会指向下标为2的地址空间,从而实现数组元素的向后移动, 也就是zend_mm_free_block->next_free_block和zend_mm_free_block->prev_free_block位置的后移。 这种不存储zend_mm_free_block数组,仅存储其指针的方式不可不说精妙。虽然在理解上有一些困难,可是节省了内存。
free_buckets列表使用free_bitmap标记是否该双向链表已经使用过期有用。 当有新的元素须要插入到列表时,须要先根据块的大小查找index, 查找到index后,在此index对应的双向链表的头部插入新的元素。
free_buckets列表的做用是存储小块内存,而与之对应的large_free_buckets列表的做用是存储大块的内存, 虽然large_free_buckets列表也相似于一个hash表,可是这个与前面的free_buckets列表一些区别。 它是一个集成了数组,树型结构和双向链表三种数据结构的混合体。 咱们先看其数组结构,数组是一个hash映射,其hash函数为:
#define ZEND_MM_LARGE_BUCKET_INDEX(S) zend_mm_high_bit(S) static void *_zend_mm_alloc_int(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) ZEND_ATTRIBUTE_MALLOC ZEND_ATTRIBUTE_ALLOC_SIZE(2); static void _zend_mm_free_int(zend_mm_heap *heap, void *p ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC); static void *_zend_mm_realloc_int(zend_mm_heap *heap, void *p, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) ZEND_ATTRIBUTE_ALLOC_SIZE(3); static inline unsigned int zend_mm_high_bit(size_t _size) { #if defined(__GNUC__) && (defined(__native_client__) || defined(i386)) unsigned int n; __asm__("bsrl %1,%0\n\t" : "=r" (n) : "rm" (_size) : "cc"); return n; #elif defined(__GNUC__) && defined(__x86_64__) unsigned long n; __asm__("bsr %1,%0\n\t" : "=r" (n) : "rm" (_size) : "cc"); return (unsigned int)n; #elif defined(_MSC_VER) && defined(_M_IX86) __asm { bsr eax, _size } #elif defined(__GNUC__) && (defined(__arm__) || defined(__aarch64__)) return (8 * SIZEOF_SIZE_T - 1) - __builtin_clzl(_size); #else unsigned int n = 0; //以上为在不一样环境下的实现 while (_size != 0) { _size = _size >> 1; n++; } return n-1; #endif }
这个hash函数用来计算size中最高位的1的比特位是多少,这点从其函数名就能够看出。 假设此时size为512Byte,则这段内存会放在large_free_buckets列表, 512的二进制码为1000000000,则zend_mm_high_bit(512)计算的值为9,则其对应的列表index为9。
在heap层下面是存储层,存储层的做用是将内存分配的方式对堆层透明化,实现存储层和heap层的分离。 在PHP的源码中有注释显示相关代码为"Storage Manager"。 存储层的主要结构代码以下:
/* Heaps with user defined storage */ typedef struct _zend_mm_storage zend_mm_storage; typedef struct _zend_mm_segment { size_t size; struct _zend_mm_segment *next_segment; } zend_mm_segment; typedef struct _zend_mm_mem_handlers { const char *name; zend_mm_storage* (*init)(void *params);//初始化函数 void (*dtor)(zend_mm_storage *storage);//析构函数 void (*compact)(zend_mm_storage *storage); zend_mm_segment* (*_alloc)(zend_mm_storage *storage, size_t size);//内存分配函数 zend_mm_segment* (*_realloc)(zend_mm_storage *storage, zend_mm_segment *ptr, size_t size);//从新分配内存函数 void (*_free)(zend_mm_storage *storage, zend_mm_segment *ptr);//内存释放函数 } zend_mm_mem_handlers; struct _zend_mm_storage { const zend_mm_mem_handlers *handlers;//处理函数集 void *data; };
以上代码的关键在于存储层处理函数的结构体,对于不一样的内存分配方案,所不一样的就是内存分配的处理函数。 其中以name字段标识不一样的分配方案。PHP在存储层共有4种内存分配方案: malloc,win32,mmap_anon,mmap_zero默认使用malloc分配内存, 若是设置了ZEND_WIN32宏,则为windows版本,调用HeapAlloc分配内存,剩下两种内存方案为匿名内存映射, 而且PHP的内存方案能够经过设置变量来修改。其官方说明以下(Zend/README.ZEND_MM):
Tweaking: --------- The Zend MM can be tweaked using ZEND_MM_MEM_TYPE and ZEND_MM_SEG_SIZE environment variables. Default values are "malloc" and "256K". Dependent on target system you can also use "mmap_anon", "mmap_zero" and "win32" storage managers. $ ZEND_MM_MEM_TYPE=mmap_anon ZEND_MM_SEG_SIZE=1M sapi/cli/php ..etc.
咱们经过一次列表的元素插入操做来理解列表的结果。 首先肯定当前须要内存所在的数组元素位置,而后查找此内存大小所在的位置。 这个查找行为是发生在树型结构中,而树型结构的位置与内存的大小有关。 其查找过程以下:
从以上的过程咱们能够画出large_free_buckets列表的结构如图
从内存分配的过程当中能够看出,内存块查找判断顺序依次是小块内存列表,大块内存列表,剩余内存列表。 在heap结构中,剩余内存列表对应rest_buckets字段,这是一个包含两个元素的数组, 而且也是一个双向链表队列,其中rest_buckets[0]为队列的头,rest_buckets[1]为队列的尾。 而咱们经常使用的插入和查找操做是针对第一个元素,即heap->rest_buckets[0], 固然,这是一个双向链表队列,队列的头和尾并无很明显的区别。它们仅仅是做为一种认知上的区分。 在添加内存时,若是所须要的内存块的大小大于初始化时设置的ZEND_MM_SEG_SIZE的值(在heap结构中为block_size字段) 与ZEND_MM_ALIGNED_SEGMENT_SIZE(等于8)和ZEND_MM_ALIGNED_HEADER_SIZE(等于8)的和的差,则会将新生成的块插入 rest_buckts所在的双向链表中,这个操做和前面的双向链表操做同样,都是从”队列头“插入新的元素。 此列表的结构和free_bucket相似,只是这个列表所在的数组没有那么多元素,也没有相应的hash函数。
在代码中,对于这4种内存分配方案,分别对应实现了zend_mm_mem_handlers中的各个处理函数。 配合代码的简单说明以下:
/* 使用mmap内存映射函数分配内存 写入时拷贝的私有映射,而且匿名映射,映射区不与任何文件关联。*/ # define ZEND_MM_MEM_MMAP_ANON_DSC {"mmap_anon", zend_mm_mem_dummy_init, zend_mm_mem_dummy_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_mmap_anon_alloc, zend_mm_mem_mmap_realloc, zend_mm_mem_mmap_free} /* 使用mmap内存映射函数分配内存 写入时拷贝的私有映射,而且映射到/dev/zero。*/ # define ZEND_MM_MEM_MMAP_ZERO_DSC {"mmap_zero", zend_mm_mem_mmap_zero_init, zend_mm_mem_mmap_zero_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_mmap_zero_alloc, zend_mm_mem_mmap_realloc, zend_mm_mem_mmap_free} /* 使用HeapAlloc分配内存 windows版本 关于这点,注释中写的是VirtualAlloc() to allocate memory,实际在程序中使用的是HeapAlloc*/ # define ZEND_MM_MEM_WIN32_DSC {"win32", zend_mm_mem_win32_init, zend_mm_mem_win32_dtor, zend_mm_mem_win32_compact, zend_mm_mem_win32_alloc, zend_mm_mem_win32_realloc, zend_mm_mem_win32_free} /* 使用malloc分配内存 默认为此种分配 若是有加ZEND_WIN32宏,则使用win32的分配方案*/ # define ZEND_MM_MEM_MALLOC_DSC {"malloc", zend_mm_mem_dummy_init, zend_mm_mem_dummy_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_malloc_alloc, zend_mm_mem_malloc_realloc, zend_mm_mem_malloc_free} static const zend_mm_mem_handlers mem_handlers[] = { #ifdef HAVE_MEM_WIN32 ZEND_MM_MEM_WIN32_DSC, #endif #ifdef HAVE_MEM_MALLOC ZEND_MM_MEM_MALLOC_DSC, #endif #ifdef HAVE_MEM_MMAP_ANON ZEND_MM_MEM_MMAP_ANON_DSC, #endif #ifdef HAVE_MEM_MMAP_ZERO ZEND_MM_MEM_MMAP_ZERO_DSC, #endif {NULL, NULL, NULL, NULL, NULL, NULL} };
假设咱们使用的是win32内存方案,则在PHP编译时,编译器会选择将ZEND_MM_MEM_WIN32_DSC宏所代码的全部处理函数赋值给mem_handlers。 在以后咱们调用内存分配时,将会使用此数组中对应的相关函数。固然,在指定环境变量 USE_ZEND_ALLOC 时,可用于容许在运行时选择 malloc 或 emalloc 内存分配。 使用 malloc-type 内存分配将容许外部调试器观察内存使用状况,而 emalloc 分配将使用 Zend 内存管理器抽象,要求进行内部调试。