因爲計算機的內存由操做系統進行管理,因此普通應用程序是沒法直接對內存進行訪問的, 應用程序只能向操做系統申請內存,一般的應用也是這麼作的,在須要的時候經過相似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 內存管理器抽象,要求進行內部調試。