背景故事
不知道大家是否还记得,我之前有聊过成立一个督促学习分享小组的设想,现在已经开始试运行了,这是我分享的第一篇内容,是对PHP内存管理的一些优秀博客的汇总和整理,作为一名搬运工,我只是希望通过整理和学习,挖掘一下PHP的底层机制。对以后定位问题,更好的学习PHP,打一个不错的基础。
插播一个广告,也欢迎大家点击阅读原文,浏览我的个人博客,会有些乱七八糟的汇总文章,希望能有一篇让你喜欢。
好了,下面开始正题。
设计思路
PHP的内存管理可以被看作是分层的。它分为三层:存储层、堆层和接口层。其中存储层通过 malloc()、mmap() 等函数向系统真正的申请内存,并通过 free() 函数释放所申请的内存。存储层通常申请的内存块都比较大,这里申请的内存大并不是指storage层结构所需要的内存大,只是堆层通过调用存储层的分配方法时,其以大块大块的方式申请的内存,存储层的作用是将内存分配的方式对堆层透明化。
设计图如下:
接口层
接口层是PHP对外提供的可调用方法,通过宏来封装了内部实现,PHP宏定义如下:
这些宏定义了一个高层次的接口,使得调用更加容易它隔离了外部调用和PHP内存管理的内部实现,实现了一种松耦合关系。
堆层
在接口层下面是PHP内存管理的核心实现,我们称之为heap层。这个层控制整个PHP内存管理的过程,首先我们看这个层的结构:
/* mm block type */
typedef struct _zend_mm_block_info {
size_t _size; /* block的大小*/
size_t _prev; /* 计算前一个块有用到*/
} zend_mm_block_info;
typedef struct _zend_mm_block {
zend_mm_block_info info;
} zend_mm_block;
typedef struct _zend_mm_small_free_block { /* 双向链表 */
zend_mm_block_info info;
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;
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;
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指定的大小,默认为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]; /* 剩余内存数组*/
};
当初始化内存管理时,调用函数是zend_mm_startup。它会初始化存储层的分配方案,初始化段大小压缩边界值,并调用zend_mm_startup_ex()初始化堆层。它对应的环境变量名为:ZEND_MM_MEM_TYPE。这里的初始化的段大小可以通过ZEND_MM_SEG_SIZE设置,如果没设置这个环境变量,程序中默认为256 * 1024。这个值存储在_zend_mm_heap结构的block_size字段中,将来在维护的三个列表中都没有可用的内存中,会参考这个值的大小来申请内存的大小。
PHP中的内存管理主要工作就是维护三个列表:小块内存列表(free_buckets)、大块内存列表(large_free_buckets)和剩余内存列表(rest_buckets)。在这里,每个bucket也对应一定大小的内存块列表,这样的列表都包含双向链表的实现。
对于小块内存, 这部分是最常用的, 所以追求高性能. 而对于大块内存, 则追求的是稳妥, 尽量避免内存浪费。对于小块内存, PHP还引入了cache机制,Zend MM 希望通过cache尽量做到, 一次定位就能查找分配:
ZEND MM是怎么管理维护cache的呢?内存在销毁前会判断是否将当前内存块放回cache的判断,如果内存的大小满足ZEND_MM_SMALL_SIZE并且cache还没有超过系统设置的ZEND_MM_CACHE_SIZE,那么就将当前内存块放回cache中。当PHP的内存溢出(即超过ini配置的内存上限时),程序会调用zend_mm_free_cache释放缓存。
关于free_buckets列表
我们可以把维护的前面两个表看作是两个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不会出现数组溢出的情况。
在内存管理初始化时,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))
这里的初始化非常巧妙,我们先看看ZEND_MM_SMALL_FREE_BUCKET,它是将free_buckets列表的偶数位的内存地址(也就是指向prev_free_block的地址)加上两个指针的内存大小并减去zend_mm_small_free_block结构所占空间的大小。而因为zend_mm_free_block结构和zend_mm_small_free_block结构的差距在于两个指针,所以他的计算结果就是free_buckets列表index对应双向链表的第一个zend_mm_free_block的prev_free_block地址减8的地址。为什么是减8的地址?因为zend_mm_free_block的前8个字节是zend_mm_block_info,之后才是prev_free_block。
为了方面理解,我们假设index为0的情况,&heap->free_buckets[0] 的地址为0x881260,加上sizeof(zend_mm_free_block) 2 再减去sizeof(zend_mm_small_free_block))的结果是0x881258,它是&heap->free_buckets[0]地址减8的地址,它指向的结构体是zend_mm_free_block,所以p->prev_free_block指向的就是0x881260,也就是heap->free_buckets[0]。
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;
}
在这个循环中,free_buckets的偶数位index,将其next_free_block和prev_free_block都指向自己,通过两个指针的大小(sizeof(zend_mm_free_block) 2)实现数组元素的向后移动,index 0->2->4->……->62 。这种不存储zend_mm_free_block数组,仅存储其指针的方式不可不说精妙。虽然在理解上有一些困难,但是节省了内存。
这个图基本上描述了关于free_buckets数组在初始化后的串联关系。
这个图是展示正常使用时,free_buckets的指向。
对于以上两张图,有什么疑问,可以和本人沟通,我的理解并非必然正确。
关于large_free_buckets列表
free_buckets列表的作用是存储小块内存,而与之对应的large_free_buckets列表的作用是存储大块的内存,虽然large_free_buckets列表也类似于一个hash表,但是这个与前面的free_buckets列表一些区别。它是一个集成了数组,树型结构和双向链表三种数据结构的混合体。我们先看其数组结构,数组是一个hash映射,其hash函数为:
这个hash函数用来计算size中最高位的1的比特位是多少,这点从其函数名就可以看出。假设此时size为512Byte,则这段内存会放在large_free_buckets列表,512的二进制码为1000000000,则zend_mm_high_bit(512)计算的值为9,则其对应的列表index为9。
我们通过一次列表的元素插入操作来理解列表的结果。首先确定当前需要内存所在的数组元素位置,然后查找此内存大小所在的位置。这个查找行为是发生在树型结构中,而树型结构的位置与内存的大小有关。其查找过程如下:
第一步 通过索引获取树型结构第一个结点并作为当前结点,如果第一个结点为空,则将内存放到第一个元素的结点位置,返回,否则转第二步。
第二步 从当前结点出发,查找下一个结点,并将其作为当前结点。
第三步 判断当前结点内存的大小与需要分配的内存大小是否一样如果大小一样则以双向链表的结构将新的元素添加到结点元素的后面第一个元素的位置。否则转四步。
第四步 判断当前结点是否为空,如果为空,则占据结点位置,结束查找,否则第二步。
内存分配策略
从内存分配的过程中可以看出,内存块查找判断顺序依次是小块内存列表,大块内存列表,剩余内存列表。在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函数。整个分配过程的流程图如下:
网友评论