PHP内存池分析

一、为什么需要内存池

     内存是非常宝贵的资源,需要最优访问;

     操作系统适合管理大块内存,如一页(4096字节),不适合小块内存分配;不做内存池管理,容易产生内存碎片,会出现剩余内存够,但没有一块连续内存来分配,会引起操作系统把程序HOLD住来整理碎片的情况;

     另外直接调用操作系统分配内存会导致从用户态切换到内核态,开销比较大;

二、内存池设计目标:

1、化零为整,减少系统调用;

2、不出现内存泄露;

3、高效,尽量无锁设计;

三、PHP内存池实现

PHP内存池分析_第1张图片

这是官方的示意图,其中free_buckets代表小块内存列表,large_free_buckets代表大块内存列表,还有一个rest_buckets, 鸟哥的解释是:"这个结构是个双向列表, 用来保存一些PHP分配后剩下的内存, 避免无意义的把剩余内存插入free_buckets带来的性能问题"。

对于小块内存, PHP还引入了cache机制:

PHP内存池分析_第2张图片

引入cache机制希望做到,一次定位就能查找分配。

其中free_bitmap和和large_free_bitmap为位图,指示对应位相应的内存索引是否有空闲内存。

下面会具体说明PHP是如何管理内存,在说明之前先说明下环境,笔记实验的机器是64位的,下面的数据都是基于这个前提。

PHP内存管理主要是围绕free_buckets和large_free_buckets这二个数组来 展开的,这二个数组都是一个长度为64的数组。

1、小块内存管理

free_buckets管理长度小于等于528字节的内存,free_buckets[0]管理长度为长度16-23字节的内存,free_buckets[1]管理长度为24-31字节的内存,依此类推……

其中每一项又是一个双向链表,讲起来比较抽象,我们来画图描述下分配和释放内存后内存的布局吧。

刚开始free_buckets数组每项都为NULL:

PHP内存池分析_第3张图片

归还32字节内存后

PHP内存池分析_第4张图片

归还36字节内存后

PHP内存池分析_第5张图片

下次假设要分配长度32-39字节之间的内存如35,直接从下标2中遍历元素,只要哪个元素的长度大于等于要分配的长度,即将长度为36的内存归还。

接下来我们看下小块内存的分配是怎么处理的,为了保证内存分配的高效,PHP每次会从操作系统分配大块内存,默认是256KB,可以通过环境变量ZEND_MM_SEG_SIZE来设置。

从操作系统分配内存后,PHP会根据前面的换算关系,将内存块放到相应的内存块中,便于后续快速分配。

2、大块内存管理

小块内存管理长度小于等于528(参考宏ZEND_MM_MAX_SMALL_SIZE的定义)字节的内存,大于528的都由large_free_buckets来管理,large_free_buckets也是长度为64的数组,每个下标管理的内存范围是前开闭区间,设下标为i,则管理内存长度为[2^i, 2^(i+1))。

举几个例子,large_free_buckets[9]的下标为9,2的9次方是512, 所以其管理长度为512-1023之间的内存;

large_free_buckets[10]管理长度为1024-2047之间的内存;large_free_buckets[11]管理长度为2048-4095之间的内存……

这样一共可以管理最大2^64的内存,当然实际不会用这么多,因为PHP有内存限制相关参数。

可以看到,在大块内存的设计时,并没有和小块内存一样每个下标管理的内存长度差为8,而是下一个下标管理的长度为上一个下标管理的长度的2倍;之所以这样设计,因为大块内存比较大,不用太细的管理,另外就是要尽量节省内存,如果相邻下标管理内存长度差为8字节,则需要很大的数组来管理这些内存。

这样设计还会有个问题,可能会造成巨大的内存浪费,如下标10管理1024-2047之间的内存,如果释放一块长度为2046的内存,但申请的时候只要1030字节,则多余的1016字节就白白浪费了,对于这个问题,PHP通过树和双向列表来管理:

PHP内存池分析_第6张图片

什么意思呢,结合释放内存的过程说明下:

1)、释放2048字节内存

     结合前面讲的,落在下标的11元素上,2048的二进制是100000000000,其中第1个1表示落在哪个下标中,这里从右到左数排第12位,从0开始计算就是第11位;

       从左到右数第二位是0,所以放到下标1这个元素的左子树上。

PHP内存池分析_第7张图片

2、释放3100字节内存

3100的二进制是110000011100,从左到右数第二位是1,所以放在右子树上。

PHP内存池分析_第8张图片

3、释放4093字节内存

      4093的二进制是111111111101,从左到右第二位是1,放右子树上,发现右子树已经有了3100,再往右数,第三位还是1,所以放到3100的右子树上

PHP内存池分析_第9张图片

申请的时候就是扫这个顺序扫描的,如果对应二进制位为0,则扫描左子树,如果为1则扫描右子树。

举个例子,这时如果要申请2900字节内存,转成二进制为101101010100,从左往右第2位为1,所以走到3100这里就返回了,而不会分配到4093字节的内存。

四、总结

1、PHP的内存分配主要是围绕两个数组来展开:free_buckets和large_free_buckets,其中前者用来管理小块内存,后者用来管理大块内存。

2、对于小块内存,做到尽量可以再次使用,分成64个区段,每段管理的内存字节间隔为8,即下标为0管理16-23,下标1管理24-31,依此类推……

3、对于大块内存,数组不宜过大,所以数组的长度也是64,但是为了不浪费内存,采用树+双向列表来管理,方便快速查找,也不会浪费太多内存。

4、内存分配时先从操作系统分配较大块内存,分配完后放入上述相应的数组中,方便下次使用。

分布式ID算法&实现

直播评论系统分析设计

一次线上Mysql死锁分析

PHP内存池分析_第10张图片

你可能感兴趣的:(php,内存管理)