PHP深入理解-記憶體管理
記憶體管理的主要目的:提高記憶體利用率,減少記憶體碎片,提高記憶體分配的速度.
記憶體設計
最大的可分配單元是chunk
,大小為2MB.每個chunk分配了512個page
,page的大小為4KB.
zend的page_size與Linux一般的page_size大小相等.
zend內部使用了mmap分配記憶體,不足一記憶體頁的mmap強制以Linux的page_size進行對齊.
mmap可以指定這塊記憶體在不同程序之間的處理方式,是共享還是寫時拷貝.
相較於malloc分配方式,提供了檔案到記憶體對映的功能.malloc分配不是基於page_size對齊的.
#define ZEND_MM_CHUNK_SIZE (2 * 1024 * 1024) /* 2 MB */
#define ZEND_MM_PAGE_SIZE (4 * 1024) /* 4 KB */
#define ZEND_MM_PAGES (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) /* 512 */
記憶體分配三種模式
- small:小於等於3KB的記憶體
- large:大於3KB小於等於(2MB減去4KB)記憶體
- huge:大於2MB減去4KB記憶體
每個chunk中的4KB用於zend_mm_chunk結構體分配.
記憶體對齊
記憶體對其的意義:根據給定的記憶體地址快速定位在申請記憶體的位置.
實際上再申請的時候為了保證記憶體對齊,會多申請一塊記憶體,然後在這塊記憶體找到對齊的地址,除這個地址+申請的地址大小外的記憶體都將要被釋放.
zend提供了以下三種計算對齊的方式.
#define ZEND_MM_ALIGNED_OFFSET(size, alignment) \
(((size_t)(size)) & ((alignment) - 1)) //得出基於對齊的起始地址的offset
#define ZEND_MM_ALIGNED_BASE(size, alignment) \
(((size_t)(size)) & ~((alignment) - 1)) //得出對齊的起始地址
#define ZEND_MM_SIZE_TO_NUM(size, alignment) \
(((size_t)(size) + ((alignment) - 1)) / (alignment)) //根據size,alignment得出需分配的個數
記憶體資料結構
zend_mm_heap:
全域性變數alloc_globals.mm_heap指向zend_mm_heap資料結構
struct _zend_mm_heap {
#if ZEND_MM_CUSTOM
int use_custom_heap;
#endif
#if ZEND_MM_STORAGE
zend_mm_storage *storage;
#endif
#if ZEND_MM_STAT
size_t size; /* current memory usage */
size_t peak; /* peak memory usage */
#endif
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
#if ZEND_MM_STAT || ZEND_MM_LIMIT
size_t real_size; /* current size of allocated pages */
#endif
#if ZEND_MM_STAT
size_t real_peak; /* peak size of allocated pages */
#endif
#if ZEND_MM_LIMIT
size_t limit; /* memory limit */
int overflow; /* memory overflow flag */
#endif
zend_mm_huge_list *huge_list; /* list of huge allocated blocks */
zend_mm_chunk *main_chunk;
zend_mm_chunk *cached_chunks; /* list of unused chunks */
int chunks_count; /* number of alocated chunks */
int peak_chunks_count; /* peak number of allocated chunks for current request */
int cached_chunks_count; /* number of cached chunks */
double avg_chunks_count; /* average number of chunks allocated per request */
#if ZEND_MM_CUSTOM
union {
struct {
void *(*_malloc)(size_t);
void (*_free)(void*);
void *(*_realloc)(void*, size_t);
} std;
struct {
void *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
void *(*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
} debug;
} custom_heap;
#endif
};
chunk資料結構:
struct _zend_mm_chunk {
zend_mm_heap *heap;//AG()裡的mm_heap地址
zend_mm_chunk *next;//下一個trunk
zend_mm_chunk *prev;//之前的trunk
int free_pages; /* number of free pages */
int free_tail; /* number of free pages at the end of chunk
最後一塊連續可用的page*/
int num; //當前chunk的序號
char reserve[64 - (sizeof(void*) * 3 + sizeof(int) * 3)];
zend_mm_heap heap_slot; /* 只用於mainchunk used only in main chunk */
zend_mm_page_map free_map; /* 空閒頁的點陣圖512 bits or 64 bytes */
zend_mm_page_info map[ZEND_MM_PAGES]; /* 儲存每個頁的使用資訊,高兩位代表使用記憶體的型別,低十位區分是否連續的頁 2 KB = 512 * 4 */
};
記憶體分配釋放邏輯
如之前所述,記憶體分配分為三種不同的範圍.
huge
分配
1.申請size需要根據page_size進行對齊
2.對齊後的size再根據chunk_size大小進行對齊
3.將記憶體掛載到alloc_global.mm_heap->huge_list上
釋放: 從huge_list連結串列中刪除,呼叫munmap釋放.
large
large分配是page分配的整數倍.
1.遍歷雙向連結串列alloc_global.mm_heap->main_trunk
2.如果free_pages小於要申請的頁的個數回到1.
3.根據zend_mm_chunk->free_map查詢最優連續page(連續page個數最少,連續page編號最少).
4.如果查詢可分配的頁則返回對應的地址,並將map[page_num]標記為large記憶體
5.如果chunk都沒有可分配記憶體,就新申請一個chunk,在進行分配.
釋放:
將zend_mm_chunk->free_map[page_num],zend_mm_chunk->map[page_num]置為0.
然後修改free_pages.如果pages都釋放,那麼釋放chunk.
small分配
small型別共分為30種不同的大小.
mm_heap->free_slot陣列結構存著每個slot型別連結串列的首地址.
1.根據申請的記憶體查詢對應的規格表
2.根據規格表中的num,如果mm_heap->free_slot[num]為空則繼續下一步,如果不為空返回對應的地址,並從mm_heap->free_slot[num]指向連結串列的首地址刪除
3.申請的規格表中對應的頁數(bin_pages[bin_num])並更新mm_chunk->map[page_num]標識位為small記憶體.第一個頁需要設定map[page_num](位於map的24bit-16bit位段)設定free_slot個數.接下的連續頁的標誌位給予順序標誌(位於map的24bit-16bit位段).
釋放時:
直接插入mm_heap->free_slot當中.
GC機制
slot
依次遍歷mm_heap->free_slot.
根據slot的地址執行上述的查詢的巨集,找到對應的chunk地址以及當前slot所在的page_num,讀取map[page_num].
如果是slot連續申請的規格表中對應的頁數(bin_pages[bin_num]),那麼位於第一頁的map的24bit-16bit位段,會用於儲存這幾個連續頁的free_slot個數.其餘的頁的map的24bit-16bit位段用儲存連續頁的頁號.
如果map中的free_slot個數+1等於規格表中(bin_elements[num])存在的元素個數,那麼刪除heap->free_slot[num]連結串列中該slot.
chunk的遍歷每個page,如果是small記憶體的第一頁(map[page_num]&0x80000000),取24-16位判斷free_slot個數等於這個規格表規定的slot個數.釋放這幾個連續的頁.
chunk
迴圈遍歷chunk連結串列如果chunk->free_pages = 511那麼就釋放這個chunk.