Press "Enter" to skip to content

PHP原理之内存管理中难懂的几个点

PHP的内存管理, 分为俩大部分, 第一部分是PHP自身的内存管理, 这部分主要的内容就是引用计数, 写时复制, 等等面向应用的层面的管理. 而第二部分就是今天我要介绍的, zend_alloc中描写的关于PHP自身的内存管理, 包括它是如何管理可用内存, 如何分配内存等.
另外, 为什么要写这个呢, 因为之前并没有任何资料来介绍PHP内存管理中使用的策略, 数据结构, 或者算法. 而在我们平时开发扩展, 修复PHP的bug的时候, 却对这一部分的知识需要有一个良好的理解. PHP开发组内的很多朋友也对这块不是很清楚, 所以我觉得有必要专门写一下.
一些基本的概念, 我就不赘述了, 因为看代码很容易能看懂, 我这里就主要介绍几个看代码没那么容易看懂的点, 为什么这么说呢, 呵呵, 我在写文章之前, 查找了下已有的资料, 已避免重复功, 其中看到了TIPI项目对这部分的描述, 发现其中错误很多. 所以, 我想这部分就是看代码也没那么容易看懂的点 🙂
目前, 英文版的介绍也在撰写中: Zend MM
Zend Memory Manager, 以下简称Zend MM, 是PHP中内存管理的逻辑. 其中有一个关键数据结构: zend_mm_heap:

Zend MM把内存非为小块内存和大块内存俩种, 区别对待, 对于小块内存, 这部分是最最常用的, 所以追求高性能. 而对于大块内存, 则追求的是稳妥, 尽量避免内存浪费.
所以, 对于小块内存, PHP还引入了cache机制:

Zend MM 希望通过cache尽量做到, 一次定位就能查找分配.
而一个不容易看懂的点是free_buckets的申明:
Q: 为什么free_buckets数组的长度是ZEND_MM_NUMBER_BUCKET个?
A: 这是因为, PHP在这处使用了一个技巧, 用一个定长的数组来存储ZEND_MM_NUMBER_BUCKET个zend_mm_free_block, 如上图中红色框所示. 对于一个没有被使用的free_buckets的元素, 唯一有用的数据结构就是next_free_block和prev_free_block, 所以, 为了节省内存, PHP并没有分配ZEND_MM_NUMBER_BUCKET * sizeof(zend_mm_free_block)大小的内存, 而只是用了ZEND_MM_NUMBER_BUCKET * (sizeof(*next_free_block) + sizeof(*prev_free_block))大小的内存..
我们来看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 保证只会使用prev和next俩个指针, 所以不会造成内存读错..
那么, 第二个不容易看懂的点, 就是PHP对large_free_buckets的管理, 先介绍分配(TIPI项目组对此部分的描述有些含糊不清):

static zend_mm_free_block *zend_mm_search_large_block(zend_mm_heap *heap, size_t true_size)

large_free_buckets可以说是一个建树和双向列表的结合:

large_free_buckets使用一个宏来决定某个大小的内存, 落在什么index上:

#define ZEND_MM_LARGE_BUCKET_INDEX(S) zend_mm_high_bit(S)

zend_mm_high_bit获取true_size中最高位1的序号(zend_mm_high_bit), 对应的汇编指令是bsr(此处, TIPI项目错误的说明为: "这个hash函数用来计算size的位数,返回值为size二进码中1的个数-1").
也就是说, 每一个在large_free_buckets中的元素, 都保持着指向一个大小为在对应index处为1的size的内存块的指针. 诶, 有点绕口, 举个例子:
比如对于large_free_buckets[2], 就只会保存, 大小在0b1000到0b1111大小的内存. 再比如: large_free_buckets[6], 就保存着大小为0b10000000到0b11111111大小的内存的指针.
这样, 再分配内存的时候, Zend MM就可以快速定位到最可能适合的区域来查找. 提高性能.
而, 每一个元素又同时是一个双向列表, 保持着同样size的内存块, 而左右孩子(child[0]和child[1])分别代表着键值0和1, 这个键值是指什么呢?
我们来举个例子, 比如我向PHP申请一个true_size为0b11010大小的内存, 经过一番步骤以后, 没有找到合适的内存, PHP进入了zend_mm_search_large_block的逻辑来在large_free_buckets中寻找合适的内存:
1. 首先, 计算true_size对应的index, 计算方法如之前描述的ZEND_MM_LARGE_BUCKET_INDEX
2. 然后在一个位图结构中, 判断是否存在一个大于true_size的可用内存已经存在于large_free_buckets, 如果不存在就返回:

size_t bitmap = heap->large_free_bitmap >> index;
if (bitmap == 0) {
   return NULL;
}

3. 判断, free_buckets[index]是否存在可用的内存:

if (UNEXPECTED((bitmap & 1) != 0))

4. 如果存在, 则从free_buckets[index]开始, 寻找最合适的内存, 步骤如下:
4.1. 从free_buckets[index]开始, 如果free_buckets[index]当前的内存大小和true_size相等, 则寻找结束, 成功返回.
4.2. 查看true_size对应index后(true_size << (ZEND_MM_NUM_BUCKETS - index))的当前最高位, 如果为1. 则在free_buckets[index]->child[1]下面继续寻找, 如果free_buckets[index]->child[1]不存在, 则跳出. 如果true_size的当前最高位为0, 则在free_buckets[index]->child[0]下面继续寻找, 如果free_buckets[index]->child[0]不存在, 则在free_buckets[index]->child[1]下面寻找最小内存(因为此时可以保证, 在free_buckets[index]->child[1]下面的内存都是大于true_size的)
4.3. 出发点变更为2中所述的child, 左移一位ture_size.
5. 如果上述逻辑并没有找到合适的内存, 则寻找最小的"大块内存":

   /* Search for smallest "large" free block */
    best_fit = p = heap->large_free_buckets[index + zend_mm_low_bit(bitmap)];
    while ((p = p->child[p->child[0] != NULL])) {
        if (ZEND_MM_FREE_BLOCK_SIZE(p) < ZEND_MM_FREE_BLOCK_SIZE(best_fit)) {
            best_fit = p;
        }
    }

注意上面的逻辑, (p = p->child[p->child[0] != NULL]), PHP在尽量寻找最小的内存.
为什么说, large_free_buckets是个键树呢, 从上面的逻辑我们可以看出, PHP把一个size, 按照二进制的0,1做键, 把内存大小信息反应到了键树上, 方便了快速查找.
另外, 还有一个rest_buckets, 这个结构是个双向列表, 用来保存一些PHP分配后剩下的内存, 避免无意义的把剩余内存插入free_buckets带来的性能问题(此处, TIPI项目错误的描述为: "这是一个只有两个元素的数组。 而我们常用的插入和查找操作是针对第一个元素,即heap->rest_buckets[0]").

28 Comments

  1. log4geek.cc
    log4geek.cc February 25, 2017

    欢迎各位PHP同道中人加入交流技术。
    高级PHP技术交流群 11153486

  2. h5xtnx46
    h5xtnx46 February 21, 2017
  3. blazer jacket theory
    blazer jacket theory April 22, 2015

    Hello it’s me, I am also visiting this web page daily, this web page is genuinely good and the
    visitors are actually sharing good thoughts.

  4. ww
    ww October 23, 2013

    这是用php写的吗?

  5. more
    more May 18, 2013

    mark

  6. Anonymous
    Anonymous May 18, 2013

    mark

  7. 小灰马
    小灰马 March 21, 2013

    之前的问题更正一下,ZEND_MM_MAX_SMALL_SIZE并非256,而是((ZEND_MM_NUM_BUCKETS<<ZEND_MM_ALIGNMENT_LOG2)+ZEND_MM_ALIGNED_MIN_HEADER_SIZE),比256稍大一点,但是也是存在之前提的那个问题,large_buckets前面几个bucket都是没用的。

  8. 小灰马
    小灰马 March 21, 2013

    鸟哥你好,对于large_free_buckets我看代码后与你的理解有些出入,希望探讨一下:对于zend_mm_high_bit,我的理解是它判断size二进制最高位的1是第几位,例如0100 0000 则对应的index应该是7,而large_free_buckets存储的内存块大小都是>ZEND_MM_MAX_SMALL_SIZE,在32位机器上,也就是256字节,这样的话,large_fee_buckets存储的最小的内存块应该大于等于256字节,也就是0b1 0000 0000,其对应的index应该是8,那么large_free_buckets前面的0~7个buckets应该是空的,没用用到才对,这样的话,你上面的的图中对于large_free_buckets的序号0~7下面就不应有节点才对,希望回复,谢谢~

  9. 小灰马
    小灰马 March 18, 2013

    其实,我看php内存管理纯粹是抄袭dlmalloc的,基本完全一样。鸟哥说是不是

  10. Anonymous
    Anonymous December 16, 2012

    帮忙分析下
    #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))
    真的不理解为什么要减去sizeof(zend_mm_small_free_block)
    这个结构是什么意思,抓狂了,鸟哥,帮帮忙

  11. gerald
    gerald May 28, 2012

    你好,想请教一下,在MVC中的数据层类中,方法和属性都用static 是否是更好的选择?因为这样速度快,而且可以省一个new,不知道这样做有没有潜在隐患,比如内存问题

  12. bitsucker
    bitsucker April 12, 2012

    这段不是很理解,请大神指教:
    “(p = p->child[p->child[0] != NULL]), PHP在尽量寻找最小的内存.”
    我理解的是沿着child[0]走才能找到比父节点更小的内存;但这个逻辑却是只要发现child[0]方向有路就不走了……咋回事呢?

  13. wedgwood
    wedgwood March 18, 2012

    写的真不错。很清晰的阐述了内存管理这块内容,看完之后很有收获。
    一个问题,感觉ZEND_MM_SMALL_FREE_BUCKET那块的看了好久才想明白,可能是我经验不足。不过,如果加上类似“这个entry是双向链表的头节点,只包含prev和next两个元素,为了用统一用(zend_mm_free_block *)指针遍历双向链表的节点,所以会将这个头结点转换成了zend_mm_free_block指针。”的说明,可能会对新手好理解很多。:)

  14. eric
    eric March 3, 2012

    很有收获。但对位图(bitmap)在这里的作用还是比较模糊,继续读代码去。

  15. Rhythm
    Rhythm December 31, 2011

    膜拜!这个东东,看来得多读几遍才能理解。

  16. henosteven
    henosteven December 3, 2011

    估计的看几遍 , 才会懂

  17. liexusong
    liexusong November 10, 2011

    不太好懂~

  18. Qiang
    Qiang November 10, 2011

    大神,你的那个示意图画得不怎么直观啊:)

  19. 浪
    November 9, 2011

    这个必须得顶啊

  20. taylor
    taylor November 9, 2011

    最近在看php的源代码,觉得好多东西都得再看几遍才能理解

  21. walu
    walu November 9, 2011

    先mark,慢慢看

Comments are closed.