Press "Enter" to skip to content

深入理解PHP内存管理之谁动了我的内存

首先让我们看一个问题: 如下代码的输出,

var_dump(memory_get_usage());
$a = "laruence";
var_dump(memory_get_usage());
unset($a);
var_dump(memory_get_usage());

输出(在我的个人电脑上, 可能会因为系统,PHP版本,载入的扩展不同而不同):

int(90440)
int(90640)
int(90472)

注意到 90472-90440=32, 于是就有了各种的结论, 有的人说PHP的unset并不真正释放内存, 有的说, PHP的unset只是在释放大变量(大量字符串, 大数组)的时候才会真正free内存, 更有人说, 在PHP层面讨论内存是没有意义的.
那么, 到底unset会不会释放内存? 这32个字节跑哪里去了?

要回答这个问题, 我将从俩个方面入手:

这32个字节去哪里了

首先我们要打破一个思维: PHP不像C语言那样, 只有你显示的调用内存分配相关API才会有内存的分配.
也就是说, 在PHP中, 有很多我们看不到的内存分配过程.
比如对于:

$a = "laruence";

隐式的内存分配点就有:

1. 为变量名分配内存, 存入符号表
2. 为变量值分配内存

所以, 不能只看表象.
第二, 别怀疑,PHP的unset确实会释放内存(当然, 还要结合引用和计数, 这部分的内容请参看我之前的文章深入理解PHP原理之变量分离/引用), 但这个释放不是C编程意义上的释放, 不是交回给OS.
对于PHP来说, 它自身提供了一套和C语言对内存分配相似的内存管理API:

emalloc(size_t size);
efree(void *ptr);
ecalloc(size_t nmemb, size_t size);
erealloc(void *ptr, size_t size);
estrdup(const char *s);
estrndup(const char *s, unsigned int length);

这些API和C的API意义对应, 在PHP内部都是通过这些API来管理内存的.
当我们调用emalloc申请内存的时候, PHP并不是简单的向OS要内存, 而是会像OS要一个大块的内存, 然后把其中的一块分配给申请者, 这样当再有逻辑来申请内存的时候, 就不再需要向OS申请内存了, 避免了频繁的系统调用.
比如如下的例子:

<?php
var_dump(memory_get_usage(TRUE)); //注意获取的是real_size
$a = "laruence";
var_dump(memory_get_usage(TRUE));
unset($a);
var_dump(memory_get_usage(TRUE));

输出:

int(262144)
int(262144)
int(262144)

也就是我们在定义变量$a的时候, PHP并没有向系统申请新内存.
同样的, 在我们调用efree释放内存的时候, PHP也不会把内存还给OS, 而会把这块内存, 归入自己维护的空闲内存列表. 而对于小块内存来说, 更可能的是, 把它放到内存缓存列表中去(后记, 某些版本的PHP, 比如我验证过的PHP5.2.4, 5.2.6, 5.2.8, 在调用get_memory_usage()的时候, 不会减去内存缓存列表中的可用内存块大小, 导致看起来, unset以后内存不变, 见评论).
现在让我来回答这32个字节跑哪里去了, 就向我刚才说的, 很多内存分配的过程不是显式的, 看了下面的代码你就明白了:

<?php
var_dump("I am Laruence, From http://www.laruence.com");
var_dump(memory_get_usage());
$a = "laruence";
var_dump(memory_get_usage());
unset($a);
var_dump(memory_get_usage());

输出:

string(43) "I am Laruence, From http://www.laruence.com"
int(90808) //赋值前
int(90976)
int(90808) //是的, 内存正常释放了

90808-90808 = 0, 正常了, 也就是说这32个字节是被输出函数给占用了(严格来说, 是被输出的Header占用了)

只增不减的数组

Hashtable是PHP的核心结构(了解Hashtable, 可以参看我之前的文章深入理解PHP之数组(遍历顺序)), 数组也是用她来表示的, 而符号表也是一种关联数组, 对于如下代码:

var_dump("I am Laruence, From http://www.laruence.com");
var_dump(memory_get_usage());
$array = array_fill(1, 100, "laruence");
foreach ($array as $key => $value) {
    ${$value . $key} = NULL;
}
var_dump(memory_get_usage());
foreach ($array as $key=> $value) {
    unset(${$value . $key});
}
var_dump(memory_get_usage());

我们定义了100个变量, 然后又按个Unset了他们, 来看看输出:

string(43) "I am Laruence, From http://www.laruence.com"
int(93560)
int(118848)
int(104448)

Wow, 怎么少了这么多内存?
这是因为对于Hashtable来说, 定义它的时候, 不可能一次性分配足够多的内存块, 来保存未知个数的元素, 所以PHP会在初始化的时候, 只是分配一小部分内存块给HashTable, 当不够用的时候再RESIZE扩容,
而Hashtable, 只能扩容, 不会减少, 对于上面的例子, 当我们存入100个变量的时候, 符号表不够用了, 做了一次扩容, 而当我们依次unset掉这100个变量以后, 变量占用的内存是释放了(118848 - 104448), 但是符号表并没有缩小, 所以这些少的内存是被符号表本身占去了...
现在, 你是不是对PHP的内存管理有了一个初步的认识了呢?

46 Comments

  1. rambone
    rambone November 3, 2016

    鸟哥讲的是都懂了 但是有个疑问 如果这个脚本是常驻内存的或者执行crontab任务 即使及时unset、gc_collect_cycles了 由于脚本没结束所以不会把内存还给os,最后还会触发memory_limit 从而导致“PHP Fatal error: Allowed memory size of 268435456 bytes exhausted ”内存泄漏~

  2. Actrace
    Actrace April 12, 2015

    “就不再需要向OS申请内存了, 避免了频繁的系统调用.”
    为什么要这么设计呢?向系统调用与自己处理调用,还不都是要处理调用,如果自己处理不好而产生了泄漏不久麻烦了?
    从性能上看这块似乎并没有啥提升啊?

    • cainiao
      cainiao March 3, 2019

      内存池了解一下

  3. […] 文章深入理解PHP内存管理之谁动了我的内存之后知道了原来unset确实会释放内存,但释放的内存并没有像C> […]

  4. pangee
    pangee March 10, 2014

    半夜查问题,又来膜拜了下此文。

  5. 刘纪君
    刘纪君 April 28, 2013

    @wclssdn
    var_dump(memory_get_usage());
    $a = “laruence”;
    var_dump(memory_get_usage());
    unset($a);
    var_dump(memory_get_usage());
    /**
    int(97556) int(97696) int(97556)
    **/
    #unset($a);
    $a=null;
    /**
    int(97528) int(97668) int(97632)
    **/
    97632-97528=4
    这四个字节应该是保存的$a的变量符号,看来要消耗一个变量只是赋值null没有真正的消耗,unset才可以

  6. doufu
    doufu September 12, 2012

    学习了,顺便解决了个问题。

  7. katelyn
    katelyn August 16, 2012

    谢谢,让我对php了解更多。

  8. Bill
    Bill July 31, 2012

    高人,追根究底,
    有营养。

  9. shirne
    shirne June 15, 2012

    学习了。
    又搞懂了不少。

  10. 路过
    路过 May 29, 2012

    楼主 我机子上面安装的PHP Version 5.3.10
    执行
    var_dump(memory_get_usage());
    $a = “laruence”;
    var_dump(memory_get_usage());
    unset($a);
    var_dump(memory_get_usage());
    输出这个:
    int 322136
    int 322240
    int 322136
    是不是说这个版本的php内存管理 进步了

  11. an
    an March 7, 2012

    Too bad this blog isn’t in english! google translate sucks at this…..a lot of good info, but unfortunately not very available.

  12. Forever
    Forever August 19, 2011

    5000个数组,每个数组里面还有的信息较多。
    执行了17W多的数据库操作,和40多秒的数据库执行时间
    采用循环操作。
    每执行完一个数组的操作内存逐渐增加。
    照理说每次循环用到得局部变量都一样。
    只有保存数据库操作结果的数组有增加,可是它的增加的量小。
    不可能需要那么大的内存。
    那这些额外的内存占用从哪里来呢?
    直到该函数调用完。这些内存空间都没释放,这是为什么呢?

  13. Forever
    Forever August 19, 2011

    博主,我这里有个处理大数组的问题。数组大小有40M.
    在我执行完一个函数完,并且unset数组后。为什么内存没有降下来呢。是你说的符号表占用的吗?
    那有什么办法把符号表占用的内存释放掉呢?

  14. 游客
    游客 July 21, 2011

    分析得很好,但问题的解析没看明白(可能有点跳跃了)。
    用这个例子可能更明白一点:
    $a = 0; $b = 0; $c = 0;
    $a = memory_get_usage();
    $s = “aaaaaaaaaaaaaaaaa”;
    $b = memory_get_usage();
    unset($s);
    $c = memory_get_usage();
    var_dump($a);
    var_dump($b);
    var_dump($c);
    输出:
    int(321584)
    int(321696)
    int(321584)
    原因:
    第一行的var_dump(memory_get_usage());
    相当于:
    $tmp = memory_get_usage();
    var_dump($tmp);
    32是被memory_get_usage之后的var_dump(的header)emalloc的(这部分没有unset)。

  15. cloud
    cloud May 29, 2011

    int(84168) int(84336) int(84168)
    我刚刚测试的结果
    这个可能其他原因引起的吧

  16. wclssdn
    wclssdn May 27, 2011

    谢谢解答~~~

  17. 雪候鸟
    雪候鸟 May 25, 2011

    @wclssdn 一般你不用关心这个问题,不过既然你问了,我举个例子, 比如你申请向PHP申请1个字节的内存, PHP可能会向OS申请一大块内存, 比如4K的, 当你归还这1字节的内存的时候, PHP发现这块4k的内存, 没有其他地方再使用了, 就会把这块内存交回给OS, 当然, 一般情况下, 这个概率很小, 因为还存在一个PHP内存Cache的机制.

  18. wclssdn
    wclssdn May 25, 2011

    那 内存管理器 可能在某个PHP文件未执行完的时候提前关闭么?
    我的意思是说.. 某个PHP文件,开始执行.. 一直申请内存. 直到整个PHP文件中的所有代码都执行完. PHP进程退出.. 才把内存还给OS是么?

  19. 雪候鸟
    雪候鸟 May 25, 2011

    @wclssdn 是的, 对于”PHP肯定不会释放内存给OS是么?”, 答案是不是的, 一块内存segment使用完毕以后, 内存管理器关闭以后,都会返回给OS的

  20. wclssdn
    wclssdn May 24, 2011

    那$var = null; 和unset($var); 有什么区别么?
    如果说. 对PHP自身的内存管理来说. 是不是unset($var);会好一些呢? 是不是变量表中已经把$var这个变量干掉了? 而$var = null; 还是会在变量表中存在$var 这个变量?
    还有就是.. PHP肯定不会释放内存给OS是么?

  21. reiko
    reiko April 4, 2011

    写的太好了。又长见识了。

  22. 雪候鸟
    雪候鸟 March 5, 2011

    @zhou 改了,不过是二十有八了,,, 🙂

  23. zhou
    zhou March 4, 2011

    话说…前年看过你的blog了… 就是”年二十有七” 怎么现在还是哇… 哈哈哈

  24. bjmayor
    bjmayor March 4, 2011

    鸟哥的文章越写越好了,我辈楷模呀。

  25. 蜡烛公爵
    蜡烛公爵 March 4, 2011

    @雪候鸟 多谢指教

  26. 雪候鸟
    雪候鸟 March 4, 2011

    @蜡烛公爵 这个可能和你使用的PHP版本也有关系, PHP的某些版本, 在调用get_memory_usage的时候, 不会减去cache中的内存块size. 比如php5.2.4, php5.3.0rc1

  27. 蜡烛公爵
    蜡烛公爵 March 4, 2011

    试验了,结果如下:
    var_dump(memory_get_usage());
    $a = “laruence”;
    var_dump(memory_get_usage());
    unset($a);
    var_dump(memory_get_usage());
    *************结果*******************
    int(55784)
    int(55888)
    int(55888)
    unset之前和之后没变
    var_dump(memory_get_usage());
    $a = “”;
    var_dump(memory_get_usage());
    unset($a);
    var_dump(memory_get_usage());
    ***********结果********************
    int(55776)
    int(55880)
    int(55896)
    unset之后比unset之前占用的还多
    这是何原因?如果说32字节各个系统各异,但unset完内存占用不但不降反而多出来了,这怎么解释?

  28. shiny
    shiny March 4, 2011

    和坛子里的某个人形成鲜明对比。

  29. horseluke
    horseluke March 4, 2011

    @雪候鸟 多谢,我研究一下

  30. maker
    maker March 4, 2011

    精彩,这部分东西还真是第一次接触到,希望博主写更多的好文。

  31. 雪候鸟
    雪候鸟 March 4, 2011

    @horseluke getrusage — Gets the current resource usages
    说明
    array getrusage ([ int $who = 0 ] )
    This is an interface to getrusage(2). It gets data returned from the system call.

  32. horseluke
    horseluke March 4, 2011

    话说,我更加看重CPU占用多一点,可惜只能从xhprof的cpu时间里面来猜算……我的思路对否?又或者,如何才能正确分析php的cpu占用问题呢……

  33. gsid
    gsid March 4, 2011

    同上,支持出书!

  34. 飞晏
    飞晏 March 4, 2011

    以前C语言虽然学的不错,但是大二大三开始都开始接触高级语言并且被填鸭式的接受。现在看了你的很多文章发现如果我走PHP路线的话还是得把C语言搞的很熟~。很受启发!

  35. 飞晏
    飞晏 March 4, 2011

    太详细了,“锱铢必较”的程序员才是真正大牛,建议出书。

Leave a Reply to 有趣的PHP内存管理 | 疯狂的火星人 Cancel reply

Your email address will not be published. Required fields are marked *