诊断内存泄漏-允许的内存大小已耗尽#个字节


98

我遇到了可怕的错误消息,可能是经过艰苦的努力,PHP的内存已用完:

第123行的file.php中的####字节已用完的允许内存大小(尝试分配####字节)

增加限制

如果您知道自己在做什么并且想要增加限制,请参见memory_limit

ini_set('memory_limit', '16M');
ini_set('memory_limit', -1); // no limit

谨防!您可能只是在解决症状,而没有解决问题!

诊断泄漏:

错误消息指向带有循环的行,我认为这是正在泄漏或不必要地累积内存。我memory_get_usage()在每次迭代结束时都打印了语句,可以看到数量缓慢增长直至达到极限:

foreach ($users as $user) {
    $task = new Task;
    $task->run($user);
    unset($task); // Free the variable in an attempt to recover memory
    print memory_get_usage(true); // increases over time
}

出于这个问题的目的,我们假设可以想象的最糟糕的意大利面条代码隐藏在全局范围内 $userTask

哪些工具,PHP技巧或调试伏都教可以帮助我发现并解决问题?


PS-我最近遇到了这种确切类型的问题。不幸的是,我还发现php有一个子对象销毁问题。如果取消设置父对象,则不会释放其子对象。必须确保我使用的修改后的unset包括对所有子对象__destruct的递归调用,依此类推。此处的详细信息:paul-m-jones.com/archives/262 ::我正在执行以下操作:function super_unset($ item){if(is_object($ item)&& method_exists($ item,“ __destruct”)){$ item-> __ destruct(); } unset($ item); }
乔什(Josh)2010年

Answers:


48

PHP没有垃圾收集器。它使用引用计数来管理内存。因此,内存泄漏的最常见来源是循环引用和全局变量。恐怕,如果您使用框架,则将需要大量代码来查找框架。最简单的工具是有选择地发出调用memory_get_usage并将其范围缩小到代码泄漏的位置。您也可以使用xdebug创建代码跟踪。运行带有执行跟踪和的代码show_mem_delta


3
但是请注意...生成的跟踪文件将是巨大的。我第一次在Zend Framework应用程序上运行xdebug跟踪时,花了很长时间才能运行并生成大小多GB(不是kb或MB ... GB)的文件。请注意这一点。
rg88

1
是的,这很沉重.. GB的声音虽然有些大-除非您有一个大的脚本。也许尝试仅处理几行(应该足以识别泄漏)。另外,请勿在生产服务器上安装xdebug扩展。
troelskn

31
从5.3开始,PHP实际上有一个垃圾收集器。另一方面,内存分析功能已从xdebug :(
wdev 2011年

3
+1发现了泄漏!具有循环引用的类!一旦将这些引用设置为unset(),就会按预期方式对对象进行垃圾收集!谢谢!:)
rinogo 2013年

@rinogo,所以您是如何发现泄漏的?您能否分享您采取的步骤?
JohnnyQ '16

11

这是我们用来识别哪些脚本占用服务器内存最多的一个技巧。

将以下代码段保存在文件中,例如/usr/local/lib/php/strangecode_log_memory_usage.inc.php

<?php
function strangecode_log_memory_usage()
{
    $site = '' == getenv('SERVER_NAME') ? getenv('SCRIPT_FILENAME') : getenv('SERVER_NAME');
    $url = $_SERVER['PHP_SELF'];
    $current = memory_get_usage();
    $peak = memory_get_peak_usage();
    error_log("$site current: $current peak: $peak $url\n", 3, '/var/log/httpd/php_memory_log');
}
register_shutdown_function('strangecode_log_memory_usage');

通过在httpd.conf中添加以下内容来使用它:

php_admin_value auto_prepend_file /usr/local/lib/php/strangecode_log_memory_usage.inc.php

然后在以下位置分析日志文件 /var/log/httpd/php_memory_log

touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_log在Web用户可以写入日志文件之前,您可能需要这样做。


8

我曾经在一个旧脚本中注意到,即使在我的foreach循环之后,PHP仍会在范围内保持“ as”变量的作用。例如,

foreach($users as $user){
  $user->doSomething();
}
var_dump($user); // would output the data from the last $user 

自从我看到以后,我不确定将来的PHP版本是否会解决此问题。如果是这种情况,您可以unset($user)在该doSomething()行之后从内存中清除它。YMMV。


13
PHP不会像C / Java / etc那样限制循环/条件。即使退出循环/条件(根据设计[?]),在循环/条件内声明的任何内容仍在范围内。另一方面,方法/函数的范围是您所期望的-函数执行结束后,所有内容/函数都会被释放。
弗兰克·法默

我假设这是设计使然。它的一个好处是,在循环之后,您可以处理找到的最后一个项目,例如,满足特定条件的项目。
joachim

可以unset(),但是请记住,对于对象,您所做的只是更改变量所指向的位置-您实际上尚未将其从内存中删除。一旦超出范围,PHP就会自动释放内存,因此更好的解决方案(就此答案而言,不是OP的问题)是使用短函数,因此它们也不会从循环中挂接到该变量上长。
Rich Court

@patcoll这与内存泄漏无关。这只是数组指针的变化。在这里看一下:3a版的prismnet.com/~mcmahon/Notes/arrays_and_pointers.html
Harm Smits

7

在php中有几种可能的内存泄漏点:

  • PHP本身
  • PHP扩展
  • 您使用的PHP库
  • 你的PHP代码

没有深入的逆向工程或php源代码知识,很难找到并修复前三个。对于最后一个,您可以使用memory_get_usage使用二进制搜索来查找内存泄漏代码


91
您的答案大概就可以了
TravisO

2
可惜的是,即使是php 7.2,他们也无法修复核心php内存泄漏。您不能在其中运行长时间运行的进程。
Aftab Naveed

6

我最近在类似的情况下在一个应用程序上遇到了这个问题。在PHP cli中运行的脚本可以循环多次迭代。我的脚本取决于几个基础库。我怀疑一个特定的库是原因,我花了几个小时徒劳地试图向其类中添加适当的销毁方法,但无济于事。面对漫长的转换到另一个库的过程(可能会遇到同样的问题),我想出了一个粗略的方法来解决我的问题。

在我的情况下,在linux cli上,我遍历了一堆用户记录,并为每个用户记录创建了我创建的多个类的新实例。我决定尝试使用PHP的exec方法创建类的新实例,以使这些过程在“新线程”中运行。这是我所指的一个非常基本的示例:

foreach ($ids as $id) {
   $lines=array();
   exec("php ./path/to/my/classes.php $id", $lines);
   foreach ($lines as $line) { echo $line."\n"; } //display some output
}

显然,这种方法有局限性,需要意识到这种方法的危险,因为创建兔子工作很容易,但是在极少数情况下,它可能有助于克服困难,直到找到更好的解决方案为止,就像我的情况一样。


6

我遇到了同样的问题,我的解决方案是用常规for替换foreach。我不确定具体细节,但似乎foreach为对象创建了一个副本(或以某种方式创建了新引用)。使用常规的for循环,您可以直接访问该项目。


5

我建议您检查php手册或添加 gc_enable()收集垃圾功能...这是内存泄漏不影响代码运行的方式。

PS:php有一个gc_enable()不带参数的垃圾收集器。


3

我最近注意到,PHP 5.3 lambda函数在删除时会留下额外的内存。

for ($i = 0; $i < 1000; $i++)
{
    //$log = new Log;
    $log = function() { return new Log; };
    //unset($log);
}

我不确定为什么,但是即使删除该函数后,每个lambda似乎也要占用250个字节。


2
我也要说同样的话。自5.3.10(#60139)起,此问题已修复-Kristopher
Ives

@KristopherIves,感谢您的更新!您是对的,这不再是问题,所以我现在不应该担心像疯了一样使用它们。
Xeoncross

2

如果您对PHP仅在函数执行后执行GC的说法是正确的,则可以将循环的内容包装在函数中作为解决方法/实验。


1
@DavidKullmann实际上我认为我的答案是错误的。毕竟,run()被调用的函数也是一个函数,最后应该发生GC。
Bart van Heukelom 2012年

2

我遇到的一个大问题是使用create_function。像在lambda函数中一样,它将生成的临时名称保留在内存中。

内存泄漏的另一个原因(在Zend Framework的情况下)是Zend_Db_Profiler。如果在Zend Framework下运行脚本,请确保将其禁用。例如,我在application.ini中具有以下内容:

resources.db.profiler.enabled    = true
resources.db.profiler.class      = Zend_Db_Profiler_Firebug

在此之前运行大约25.000个查询+处理负载,使内存达到了一个不错的128Mb(我的最大内存限制)。

只需设置:

resources.db.profiler.enabled    = false

足以将其保持在20 Mb以下

该脚本在CLI中运行,但是它实例化Zend_Application并运行Bootstrap,因此它使用了“开发”配置。

它确实有助于通过xDebug分析运行脚本


2

我没有看到它的明确提及,但是xdebug在分析时间和内存(从2.6开始方面做得很好。您可以将其生成的信息传递给您选择的gui前端:webgrind(仅时间),kcachegrindqcachegrind或其他,它会生成非常有用的调用树和图形,以使您找到各种的根源。 。

示例(qcachegrind的): 在此处输入图片说明


1

我这次谈话有点晚,但是我将分享与Zend Framework相关的内容。

安装php 5.3.8(使用phpfarm)以与使用php 5.2.9开发的ZF应用程序配合使用后,出现内存泄漏问题。我发现内存泄漏是由Apache的httpd.conf文件触发的,该文件在我的虚拟主机定义中显示为SetEnv APPLICATION_ENV "development"。在注释掉该行之后,内存泄漏停止了。我正在尝试在我的php脚本中提出一个内联解决方法(主要是通过在主index.php文件中手动定义它)。


1
问题是他正在CLI中运行。这意味着Apache完全不参与该过程。
Maxime 2012年

1
@Maxime好点了,谢谢,我没听懂。哦,好吧,希望无论如何我都会在这里留下一些便条,从而使一些随机的Googler受益,因为在尝试解决我的问题时出现了此页面。
fronzee 2012年

检查我对这个问题的回答,也许那也是您的情况。
安迪

您的应用程序应具有不同的配置,具体取决于环境。该"development"环境通常具有许多其他环境可能没有的日志记录和配置文件。注释掉该行仅使您的应用程序改为使用默认环境,通常为"production""prod"。内存泄漏仍然存在;只是在那个环境中没有调用包含它的代码。
Marco Roy

0

我在这里没有看到它,但是可能有用的一件事是使用xdebug和xdebug_debug_zval('variableName')查看引用计数。

我还可以提供一个阻碍php扩展的示例:Zend Server的Z-Ray。如果启用了数据收集,则每次迭代时都会增加内存使用量,就像关闭垃圾收集一样。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.