时间:2021-07-01 10:21:17 帮助过:8人阅读
PHP数组读取的循环操作
附:
PHP内存溢出Allowed memory size of 解决办法
PHP内存溢出Allowed memory size of 解决办法使用脚本语言最大的好处之一就是可利用其拥有的自动垃圾回收机制(释放内存)。你不需要在使用完变量后做任何释放内存的处理,PHP会帮你完成。当然,我们可以按自己的意愿调用 unset() 函数来释放内存,但通常不需要这么做。不过在PHP里,至少有一种情况内存不会得到自动释放,即便是手动调用 unset()。详情可考:http://bugs.php.com/bug.php?id=33595。问题症状如果两个对象之间存在着相互引用的关系,如“父对象-子对象”,对父对象调用 unset() 不会释放在子对象中引用父对象的内存(即便父对象被垃圾回收,也不行)。有些糊涂了?我们来看下面的这段代码:查看源码打印?01 bar = new Bar($this); 06 } 07 } 08 09 class Bar { 10 function __construct($foo = null) 11 { 12 $this->foo = $foo; 13 } 14 } 15 16 while (true) { 17 $foo = new Foo(); 18 unset($foo); 19 echo number_format(memory_get_usage()) . "\n"; 20 } 21 ?> 运行这段代码,你会看到内存使用率越来越高越来越高,直到用光光。...33,551,61633,551,97633,552,33633,552,696PHP Fatal error: Allowed memory size of 33554432 bytes exhausted(tried to allocate 16 bytes) in memleak.php on line 17对大部分PHP程序员来讲这种情况不算是什么问题。可如果你在一个长期运行的代码中使用到了一大堆相互引用的对象,尤其是在对象相对较大的情况下,内存会迅速地消耗殆尽。Userland解决方案虽然有些乏味、不优雅,但之前提到的 bugs.php.com 链接中提供了一个解决方案。这个方案在释放对象前使用一个 destructor 方法以达到目的。Destructor 方法可将所有内部的父对象引用全部清除,也就是说可以将这部分本来会溢出的内存释放掉。以下是“修复后”的代码:查看源码打印?01 bar = new Bar($this); 06 } 07 function __destruct() 08 { 09 unset($this->bar); 10 } 11 } 12 13 class Bar { 14 function __construct($foo = null) 15 { 16 $this->foo = $foo; 17 } 18 } 19 20 while (true) { 21 $foo = new Foo(); 22 $foo->__destruct(); 23 unset($foo); 24 echo number_format(memory_get_usage()) . "\n"; 25 } 26 ?> 注意那个新增的 Foo::__destruct()方法,以及在释放对象前对 $foo->__destruct() 的调用。现在这段代码解决了内存使用率一直增加的问题,这么一来,代码就可以很好的工作了。PHP内核解决方案?为什么会有内存溢出的发生?我对PHP内核方面的研究并不精通,但可以确定的是此问题与引用计数有关系。在 $bar 中引用 $foo 的引用计数不会因为父对象 $foo 被释放而递减,这时PHP认为你仍需要 $foo 对象,也就不会释放这部分的内存……大概是这样。这里确实可以看出我的无知,但大体意思是:一个引用计数没有递减,所以一些内存永远得不到释放。在前面提到的 bugs.php.com 链接中我看到修改垃圾回收的过程将会牺牲极大的性能,因为我对引用计数了解不多,所以我认为这是真的。与其改变垃圾回收的过程,为什么不用 unset() 对内部对象做释放的工作呢?(或者在释放对象的时候调用 __destruct()?)也许PHP内核开发者可以在此或其他地方,对这种垃圾回收处理机制做出修改。更新:Martin Fjordvald 在评论中提到了一个由 David Wang 为垃圾回收所写的补丁(其实它看起来更像“一整块布”??非常巨大。详情参见此邮件结尾的CVS导出信息。)确实存在(一封邮件),并受到了PHP内核开发成员的关注。问题是这个补丁要不要放到PHP5.3中并未得到太多支持 。我觉得一个不错的折中方案就是在 unset() 函数中调用对象中的 __destruct() 方法;
PHP查询MySQL大量数据的内存占用分析
PHP查询MySQL大量数据的内存占用分析作者:ideawu 出处:博客2011-07-07 14:32昨天, 有同事在PHP讨论群里提到, 他做的一个项目由于MySQL查询返回的结果太多(达10万条), 从而导致PHP内存不够用. 所以, 他问, 在执行下面的代码遍历返回的MySQL结果之前, 数据是否已经在内存中了? 这篇文章主要是从原理, 手册和源码分析在PHP中查询MySQL返回大量结果时, 内存占用的问题, 同时对使用MySQL C API也有涉及. 昨天, 有同事在PHP讨论群里提到, 他做的一个项目由于MySQL查询返回的结果太多(达10万条), 从而导致PHP内存不够用. 所以, 他问, 在执行下面的代码遍历返回的MySQL结果之前, 数据是否已经在内存中了? - 以下是代码片段: 以下是代码片段: while ($row = mysql_fetch_assoc($result)) { // ... } 当然, 这种问题有许多优化的方法. 不过, 就这个问题来讲, 我首先想到, MySQL是经典的C/S(Client/Server, 客户端/服务器)模型, 在遍历结果集之前, 底层的实现可能已经把所有的数据通过网络(假设使用TCP/IP)读到了Client的缓冲区, 也有另一种可能, 就是数据还在Server端的发送缓冲区里, 并没有传给Client. 在查看PHP和MySQL的源码之前, 我注意到PHP手册里有两个功能相近的函数: 以下是代码片段: 以下是代码片段:mysql_query() mysql_unbuffered_query() 两个函数的字面意思和说明证实了我的想法, 前一个函数执行时, 会把所有的结果集从Server端读到Client端的缓冲区中, 而后一个则没有, 这就是”unbuffered(未缓冲)”的意思. 那就是说, 如果用mysql_unbuffered_query()执行了一条返回大量结果集的SQL语句, 在遍历结果之前, PHP的内存是没有被结果集占用的. 而用mysql_query()来执行同样的语句的话, 函数返回时, PHP的内存占用便会急剧增加, 立即耗光内存. 如果阅读PHP的相关代码, 可以看到这两个函数的实现上的异同: 以下是代码片段:/* {{{ proto resource mysql_query(string query [, int link_identifier]) Sends an SQL query to MySQL */ PHP_FUNCTION(mysql_query) { php_mysql_do_query(INTERNAL_FUNCTION_PARAM_PASSTHRU, MYSQL_STORE_RESULT); } /* }}} */ /* {{{ proto resource mysql_unbuffered_query(string query [, int link_identifier]) Sends an SQL query to MySQL, without fetching and buffering the result rows */ PHP_FUNCTION(mysql_unbuffered_query) { php_mysql_do_query(INTERNAL_FUNCTION_PARAM_PASSTHRU, MYSQL_USE_RESULT); } /* }}} */ mysql_query() mysql_unbuffered_query() 两个函数的字面意思和说明证实了我的想法, 前一个函数执行时, 会把所有的结果集从Server端读到Client端的缓冲区中, 而后一个则没有, 这就是”unbuffered(未缓冲)”的意思. 那就是说, 如果用mysql_unbuffered_query()执行了一条返回大量结果集的SQL语句, 在遍历结果之前, PHP的内存是没有被结果集占用的. 而用mysql_query()来执行同样的语句的话, 函数返回时, PHP的内存占用便会急剧增加, 立即耗光内存. 如果阅读PHP的相关代码, 可以看到这两个函数的实现上的异同: 以下是代码片段: 以下是代码片段:/* {{{ proto resource mysql_query(string query [, int link_identifier]) Sends an SQL query to MySQL */ PHP_FUNCTION(mysql_query) { php_mysql_do_query(INTERNAL_FUNCTION_PARAM_PASSTHRU, MYSQL_STORE_RESULT); } /* }}} */ /* {{{ proto resource mysql_unbuffered_query(string query [, int link_identifier]) Sends an SQL query to MySQL, without fetching and buffering the result rows */ PHP_FUNCTION(mysql_unbuffered_query) { php_mysql_do_query(INTERNAL_FUNCTION_PARAM_PASSTHRU, MYSQL_USE_RESULT); } /* }}} */ 两个函数都调用了php_mysql_do_query(), 只差了第2个参数的不同, MYSQL_STORE_RESULT和MYSQL_USE_RESULT. 再看php_mysql_do_query()的实现: 以下是代码片段: 以下是代码片段:if(use_store == MYSQL_USE_RESULT) { mysql_result=mysql_use_result(&mysql->conn); } else { mysql_result=mysql_store_result(&mysql->conn); } if(use_store == MYSQL_USE_RESULT) { mysql_result=mysql_use_result(&mysql->conn); } else { mysql_result=mysql_store_result(&mysql->conn); } mysql_use_result()和mysql_store_result()是MySQL的C API函数, 这两个C API函数的区别就是后者把结果集从MySQL Server端全部读取到了Client端, 前者只是读取了结果集的元信息. 回到PHP, 使用mysql_unbuffered_query(), 可以避免内存的立即占用. 如果在遍历的过程不对结果进行”PHP缓存”(如放到某数组中), 则整个执行过程虽然操作了十万条或者百万条或者更多的数据, 但PHP占用的内存始终是非常小的.
核心提示:PHP基本上就是一种数组语言。时常要进行大量的数组循环操作,主要有两种方式,一种是foreach,另一种是while,到底哪种好哪种坏一直有争论,虽然我很早就意识到了这个问题,但是一直没有细究,懵懂的感觉一直持续到现在,为了以后能节省点CPU时间.....PHP基本上就是一种数组语言。时常要进行大量的数组循环操作,主要有两种方式,一种是foreach,另一种是while,到底哪种好哪种坏一直有争论,虽然我很早就意识到了这个问题,但是一直没有细究,懵懂的感觉一直持续到现在,为了以后能节省点CPU时间,下面总结一下:在循环里进行的是数组“读”操作,则foreach比while快:无格式查看复制到剪贴板打印代码?foreach ($array as $value) {echo $value;}while (list($key) = each($array)) {echo $array[$key];}foreach ($array as $value) {echo $value;}while (list($key) = each($array)) {echo $array[$key];}在循环里进行的是数组“写”操作,则while比foreach快:无格式查看复制到剪贴板打印代码?foreach ($array as $key => $value) {echo $array[$key] = $value . '...';}while (list($key) = each($array)) {$array[$key] = $array[$key] . '...';}foreach ($array as $key => $value) {echo $array[$key] = $value . '...';}while (list($key) = each($array)) {$array[$key] = $array[$key] . '...';}总结:通常认为,foreach涉及到值复制,一定会比while慢,但实际上,如果仅仅是在循环里进行数组的读操作,那么foreach是很快的,这是因为PHP采用的复制机制是“引用复制,写时拷贝”,这样看来,foreach的高效读操作就不难理解了。另外,既然foreach不适合处理数组写操作,那么我们可以得出一个结论,多数情况下,类似foreach ($array as $key => $value)形式的代码都应该被替换成while (list($key) = each($array))。这些技巧产生的速度差异在小项目里可能并不明显,但是在类似框架这样的大项目中,一次请求动辄便会涉及到几百几千几万次数组循环操作,差异就会明显放大。
首先让我们看一个问题: 如下代码的输出,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申请内存了, 避免了频繁的系统调用.比如如下的例子:输出:
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个字节跑哪里去了, 就向我刚才说的, 很多内存分配的过程不是显式的, 看了下面的代码你就明白了:输出: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(104448Wow, 怎么少了这么多内存?这是因为对于Hashtable来说, 定义它的时候, 不可能一次性分配足够多的内存块, 来保存未知个数的元素, 所以PHP会在初始化的时候, 只是分配一小部分内存块给HashTable, 当不够用的时候再RESIZE扩容, 而Hashtable, 只能扩容, 不会减少, 对于上面的例子, 当我们存入100个变量的时候, 符号表不够用了, 做了一次扩容, 而当我们依次unset掉这100个变量以后, 变量占用的内存是释放了(118848 ? 104448), 但是符号表并没有缩小, 所以这些少的内存是被符号表本身占去了…