Press "Enter" to skip to content

请手动释放你的资源(Please release resources manually)

我从来不认为这个问题是个问题, 直到昨天.
昨天晚上的时候, 我提交了一个RFC, 关于引入finally到PHP, 实现这个功能的出发点很简单, 因为我看见不少人的需求, 另外还有就是Stas说, 一直只看到讨论, 没看到有人实现. 于是我就给实现了.
发到邮件组以后, 一个开发组的同学Nikita Popov(nikic), 表示强烈反对这个RFC, 当然最初的论点他说了很多, 最后我们在线讨论的时候, 他表达了一个他的观点:

"PHP在请求结束后会释放所有的资源, 所以我们没有必要调用fclose,或者mysql_close来释放资源, PHP会替我们做"

并且他表示, 他从来都不会调用fclose, 认为fclose的存在只是为了继承C函数族.
我很惊讶, 我也不知道还有多少人是和他一样的想法, 所以我决定写这篇文章.
在PHP5.2以前, PHP使用引用计数(Reference count)来做资源管理, 当一个zval的引用计数为0的时候, 它就会被释放. 虽然存在循环引用(Cycle reference), 但这样的设计对于开发Web脚本来说, 没什么问题, 因为Web脚本的特点和它追求的目标就是执行时间短, 不会长期运行. 对于循环引用造成的资源泄露, 会在请求结束时释放掉. 也就是说, 请求结束时释放资源, 是一种部补救措施(backup).
然而, 随着PHP被越来越多的人使用, 就有很多人在一些后台脚本使用PHP, 这些脚本的特点是长期运行, 如果存在循环引用, 导致引用计数无法及时释放不用的资源, 则这个脚本最终会内存耗尽退出.
所以在PHP5.3以后, 我们引入了GC, 也就是说, 我们引入GC是为了解决用户无法解决的问题.
这个是历史, 我简单介绍下, 现在让我们回头来看开头的问题, 是不是因为PHP会在请求结束后释放所有的资源, 于是我们就可以不用手动释放呢?
看一个例子:

Mysql最大连接数(mysql.max_connections)

 <?php
    $db = mysql_connect() ;
    $resut = mysql_query();
    // process result...
    usleep(500);
    //mysql_close($db);  let's say, you didn't call to this
    // other logic, assuming it costs 5s
    sleep(5);
    exit(0); //finish

上面的例子, 我们会保持一个和Mysql的连接5秒钟, 这样的脚本对于一般的应用来说没有关系, 但是对于一个请求量很大的脚本来说, 会导致一个致命问题:
比如一个繁忙的应用, 每秒要处理来自用户的1000个请求, 那么5秒钟请求多少个? 5 * 1000 = 5000, 而Mysql有最大连接数限制(mysql.max_connections), 这个数字一般不超过2000, 默认的会更低:(mysql.max_connections),
那么, 这样代码会导致你的应用, 根本无法正常提供服务. 而如果我们在对Mysql的处理完成后就关闭这个连接, 那么就不会触发这个问题.
而我们在实践中, 遇到过一个更加实际的问题, 看下面的例子:

<?PHP
$mmc = new Memcached();
$mysql = mysql_connect();
//process
mysql_close($mysql);
$mmc->close();

这是一个真实的教训, 代码如上面所示, 突然有一天我们的Mysql出现了问题, 导致连接Mysql的耗时增大, 然后就导致, 一个脚本对Memcached连接占用过长, 最后Memcache因为连接数太多, 就拒绝服务了..
所以, 我们一定要让连接代价最高的资源, 最先初始化.

系统最大句柄 (/proc/sys/fs/file-max)

这个很简单, 如果你持续打开句柄, 而不释放, 那么你有可能触发系统最大句柄限制, 对于进程来说, 自己还有进程可打开句柄数限制(ulimit -n).

系统调用是昂贵的(System call is expensive)

PHP之所以会在请求结束后正确的释放掉所有的资源, 内存, 这是因为当我们在脚本中使用新的内存的时候, PHP会向OS申请一大块内存(ZEND_MM_SEG_SIZE大小), 然后分给你你需要的合适的一块小内存.
当你不使用这块小内存的时候, PHP也不会返还给OS, 而是保留下来给后续的处理使用.
我们知道, malloc(3)会导致系统调用(brk(2))(当然也可能是mmap, 我们此处不考虑这个细节, thanks to 华裔), 而系统调用是昂贵的.
所以, 如果你使用完了资源不及时释放, 那么后续的逻辑如果请求内存, PHP发现之前申请的一大块内存已经分光了, 它就只好再次向OS发起malloc调用, 得到一块新的大内存. 并且它还需要对这个大内存做一些标记处理..
而如果你使用完资源, 及时释放的话, 那么下次脚本申请内存的时候, 你之前归还的内存块就可以被重复利用, 那么也许你的整个脚本只需要和OS申请一次内存.

内存峰值(Memory peak usage)

这个和上面的有一定的关系, 当你使用完资源就释放, 然后后续又使用这样的资源. 那么PHP的内存占用会是:
资源+1 -> 资源-1 -> 资源+1 -> 资源-1 (峰值是1)
而如果你是等到PHP请求结束再释放:
资源+1 -> 资源 + 1 .... -> 资源 -1 -> 资源 - 1 (峰值是2)
也就说, 一个良好的编写的脚本可能要比一个瞎写的脚本, 要省很多峰值内存..
考虑一个极端情况, 对一个很繁忙的服务器来说, 比如有10个PHP进程, 每个PHP进程最大1G内存, 而服务器只有8G内存.

结论 (conclusion)

结论很明显, 我开头也说过了, 我从来不认为这个是个问题.
这里说一句, 如果你买了一本PHP的书, 它告诉你: "不用在PHP主动释放资源, 因为PHP会帮你释放"的话, 我建议你, 烧了它.

56 Comments

  1. 李三
    李三 April 29, 2019

    “我建议你,烧了它”,划重点

  2. 家居灯饰
    家居灯饰 May 29, 2018

    手头资源不多,应该去哪里找资源

  3. feng
    feng February 17, 2016

    同理,用完变量也要及时unset。

  4. 许胜斌
    许胜斌 February 16, 2016

  5. 伍德龙
    伍德龙 November 25, 2014

    烧书去。。

  6. change
    change July 18, 2014

    I love it whenever people get together and share ideas.
    Great website, continue the good work!

  7. There are many Lethbridge plumbing companies in this city
    which provide full service of plumbing. Because of
    the perceived “snob appeal” I mentioned earlier, however, the fact that you have copper plumbing adds resale value to
    your home. Installation and repair of bathroom and kitchen water systems.

  8. Wang Wenlin
    Wang Wenlin January 13, 2014

    C++ 有多少理由不要 finally,PHP 就有多少理由…
    Destructor, RAII 难道是只是传说的?

  9. Carl
    Carl March 11, 2013

    典型C程序员的活,不过我曾经手动释放过,PHP5.2版本,每次都遇到二次释放的错误,后来就不管了,还好没写什么大并发程序

  10. Anonymous
    Anonymous January 17, 2013

    同事说的有道理,你说的只是个例,当然最后折中后还是手动释放好一些

  11. PHP Sucks
    PHP Sucks November 29, 2012

    php做业务逻辑,就是烂泥扶不上墙。还是让它该干嘛干嘛吧!有多少大网站会在php里直接访问数据库呢?

  12. lingcarzy
    lingcarzy September 15, 2012

    坑爹了,我就是学那些可以被烧掉的书开始php的旅程,后悔死了

  13. […] $res 结果集后,一定要及时的通过mysql_free_result()方法释放资源 原因参见“请手动释放你的资源(Please release resources manually)” mysql_close() […]

  14. xbzbing
    xbzbing August 14, 2012

    我貌似有一本可以烧了的php书。。。

  15. Anonymous
    Anonymous August 8, 2012

    PHP本来在解决这方面问题就不擅长, 比如文中提到的访问mysql都是同步方式, 很容易出现连接数占用过大. 手动释放资源只是解决了一部分问题而已.针对这种情况, 我们一般会做一个代理层处理php到后端服务的连接.

  16. 0xFP
    0xFP August 7, 2012

    php -r ‘
    echo “原始内存:\t”, memory_get_usage(true) / 1024, “\n”;
    $x = str_repeat(“a”, 100 * 1024);
    echo “100 * 1024:\t”, memory_get_usage(true) / 1024, “\n”;
    $x = str_repeat(“a”, 150 * 1024);
    echo “150 * 1024:\t”, memory_get_usage(true) / 1024, “\n”;
    $x = str_repeat(“a”, 100 * 1024);
    echo “100 * 1024:\t”, memory_get_usage(true) / 1024, “\n”;
    $x = str_repeat(“a”, 300 * 1024);
    echo “300 * 1024:\t”, memory_get_usage(true) / 1024, “\n”;
    $x = str_repeat(“a”, 200 * 1024);
    echo “200 * 1024:\t”, memory_get_usage(true) / 1024, “\n”;’
    原始内存: 256 // 这些内存包括变量以外的开销
    100 * 1024: 256
    150 * 1024: 512 // 内存不足, 一次性增加了 256 Kib
    100 * 1024: 512 // ????? 相同字符串大小,但向系统申请的內存与前一个不同, 似乎变量以外的开销在使用,求原因?
    300 * 1024: 768 // 一次性增加了 256 Kib
    200 * 1024: 512 // 一次性减少了 256 Kib

  17. 0xFP
    0xFP August 7, 2012

    PHP 每次申请的内存都是一段内存(ZEND_MM_SEG_SIZE大小, php-5.4.3 是 256 Kib),当这一大块内存空闲时就会还给系统!
    一段内存可能储存放了多个变量,只有当这些变量都释放了,这段內存才是处于空闲!

  18. quminhui
    quminhui August 7, 2012

    “当你不使用这块小内存的时候, PHP也不会返还给OS, 而是保留下来给后续的处理使用.”
    请问:在PHP处理一个请求的运行周期内,这是否意味着PHP对系统内存的使用是只申请不释放?

  19. Zava
    Zava August 4, 2012

    不知道这件事情的结果怎么样?
    个人觉得还是理念的不同,往高了说,是对PHP的定位理解不同。

  20. Adeploy
    Adeploy July 31, 2012

    关于Mysql最大连接数的问题,强烈同意。有一次做一个大批量数据处理的问题,总是处理到10W多条数据就死掉,后来才发现是mysql连接没释放。。。可怜花了我好几天时间才找到bug

  21. 大笨兔
    大笨兔 July 29, 2012

    对向我这样的初学者来说可以起到提醒作用,但愿我以后会注意这方面的问题

  22. toneyzeng
    toneyzeng July 28, 2012

    非常同意博主的的意见,例如在使用smarty的场景,通常我们的作法是,执行参数判断,获取数据,smarty输出。结果很多人是这么写的:不管3721要不要用数据库,先include一个db.inc.php,然后执行参数判断,获取数据,smarty输出,最后整个网站的瓶颈就卡在众多的sql连接上。要是有提前释放资源的意识,php也就更为强大了,而finally是给try的吧,语言特性支持是没有错的,对于没有意识的人再好的工具也没用,对于会用的人就是宝贝了。

  23. lxylxy888666
    lxylxy888666 July 28, 2012

    php脚本长时间run时要注意的一个细节问题,__destory里释放也无用。

  24. Anonymous
    Anonymous July 27, 2012

    非常感谢分享这类PHP的细节问题,学习!

  25. stefan321
    stefan321 July 27, 2012

    现在注意这些细节的 phper 不多了~

  26. sk
    sk July 26, 2012

    要看什么资源。哈哈,静态变量不用释放了
    句柄需要

  27. vincent
    vincent July 25, 2012

    我觉得只是语言的设计理念的分歧而已
    设计者实现的目标不同,面向机器或者是面向编写者。
    从学习php就一直说释放资源,可是不释放是不是可以呢?为什么可以呢?

  28. 不会PHP
    不会PHP July 25, 2012

    一些细节问题,新手值得学习。

  29. Greyson
    Greyson July 25, 2012

    谁也无法保证在你使用结束后会发生什么事情,干嘛不手动释放啊,很重要。(小声问下:怎么加入邮件组,新浪员工。谢谢)

  30. lucky
    lucky July 25, 2012

    鸟哥,那么MYSQL的链接复用和即时销毁面前做取舍啊?

  31. lucky
    lucky July 25, 2012

    鸟哥,那么MYSQL的链接复用和即时销毁面前做取舍啊?

  32. ixqbar
    ixqbar July 25, 2012

    这个RFC如何打包到已经装好php上

  33. Anonymous
    Anonymous July 25, 2012

    后排支持鸟哥

  34. johnson
    johnson July 25, 2012

    谅解下年轻人哦,嘿嘿

  35. miro
    miro July 25, 2012

    这个真不是一个问题. 有啥子好争的.
    小时候大便自己不擦屁股, 长大了自己还不想擦?!

  36. Jason Gian
    Jason Gian July 25, 2012

    长见识了

  37. Forever
    Forever July 25, 2012

    看到标题有个单词错了
    “Please release resources maunally”
    是manually吧。
    其实文章还没看 先看到这个 嘿

  38. Demon
    Demon July 25, 2012

    中文:Nikita Popov 你丫朽木不可雕.
    English:Nikita Popov you’re a genius!

  39. 破月亮
    破月亮 July 25, 2012

    沙发~ 细节还是需要注意的~

Comments are closed.