【问题标题】:Force freeing memory in PHP在 PHP 中强制释放内存
【发布时间】:2010-03-17 11:23:15
【问题描述】:

在一个 PHP 程序中,我顺序读取一堆文件(带有file_get_contents),gzdecode 它们,json_decode 结果,分析内容,将大部分丢弃,并将大约 1% 存储在一个数组。

不幸的是,每次迭代(我遍历包含文件名的数组),似乎都会丢失一些内存(根据memory_get_peak_usage,每次大约 2-10 MB)。我已经对我的代码进行了双重和三重检查;我没有在循环中存储不需要的数据(所需的数据总体上几乎不超过 10MB),但我经常重写(实际上是数组中的字符串)。显然,PHP 没有正确释放内存,因此使用越来越多的 RAM 直到达到限制。

有没有办法进行强制垃圾回收?或者,至少,找出内存的使用位置?

【问题讨论】:

  • 如果我将越来越大的数据块传递给 json_dcode() 会使用更多内存(并且不会再次释放,至少在我的测试环境中没有,但目前它没有达到内存限制)。如果在同一个 php 实例中再次解析 same 数据(相同结构的数据,不必是完全相同的变量),则不会进一步增加。您提供给 json_decode() 的 json 数据的结构、“大小”和值是否变化很大?
  • 不,数据结构几乎一模一样——都是结构不变的对象数组,只是数组的长度不同。
  • memory_get_peak_usage 报告一个单调递增的值——你在程序的任何时候使用的最大内存。使用memory_get_usage(true) 获取当前实际使用的内存。
  • 这里有一篇很好的文章解释了内存使用情况arr.gr/blog/2014/05/…

标签: php garbage-collection


【解决方案1】:

这与内存碎片有关。

考虑两个字符串,连接到一个字符串。在创建输出之前,必须保留每个原件。输出比任一输入都长。
因此,必须进行新的分配来存储这种连接的结果。原始字符串已被释放,但它们是一小块内存。
'str1' . 'str2' . 'str3' . 'str4' 的情况下,您会在每个 . - 它们都不适合被释放的空间。由于内存的其他用途,这些字符串可能没有布置在连续的内存中(即,每个字符串都是,但各种字符串不是首尾相连的)。所以释放字符串会产生问题,因为空间不能有效地重用。因此,您创建的每个 tmp 都会增长。而且你永远不会重复使用任何东西。

使用基于数组的内爆,您只创建 1 个输出 - 正是您需要的长度。仅执行 1 次额外分配。因此它的内存效率更高,并且不会受到连接碎片的影响。蟒蛇也是如此。如果您需要连接字符串,则应始终基于数组进行多于 1 个连接:

''.join(['str1','str2','str3'])

在python中

implode('', array('str1', 'str2', 'str3'))

在 PHP 中

sprintf 等价物也可以。

memory_get_peak_usage 报告的内存基本上总是它必须使用的虚拟映射中内存的“最后”位。因此,由于它一直在增长,因此它报告了快速增长。由于每个分配都落在当前使用的内存块的“末尾”。

【讨论】:

  • 很好的解释,+1。
  • 上面给出的答案似乎并不完全正确。我使用 PHPParser 在我的项目(由 1000 个类和底层的 cake-framework 组成)中重写了所有 concat-Operations 到 implodes。之后,我用一个巨大的脚本对其进行了测试,该脚本正在获取我所有的模型,进行一些聚合并将其保存回数据库。 --- - 使用 Concat (.) 需要 103 秒并使用:+ 开始:24,751MB + 结束:45,985MB - 使用 Implode 大约需要 109 秒并使用:+ 开始:25,341MB + 结束: 46,660MB 我每个模型损失了大约 6kb,这就是内存使用量增加的原因。
  • @velop 很明显,您正在使用的 许多 库之一中存在一个与 PHP 工作方式无关的错误。您需要解决底层类/方法/等中的问题。
【解决方案2】:

在 PHP >= 5.3.0 中,您可以调用 gc_collect_cycles() 来强制通过 GC。

注意:您需要在您的php.ini 中启用zend.enable_gc,或者调用gc_enable() 来激活循环引用收集器。

【讨论】:

  • 我认为你需要先调用 gc_enable()。
  • PHP 5.3+ 中的垃圾收集器不是主要的内存管理机制。它专门用于处理循环引用的问题,这是基于主引用计数的系统无法处理的。所描述的场景不涉及此类循环引用,因此完全不受 GC 影响。
  • 它仍然有助于在 PHP 5.3 中结合 memory_get_usage 查看再次释放的内存。
【解决方案3】:

找到解决方案:这是一个字符串连接。我通过连接一些变量(输出是 CSV 文件)逐行生成输入。然而,PHP 似乎并没有释放用于字符串旧副本的内存,因此有效地用未使用的数据破坏了 RAM。切换到基于数组的方法(并在将其 fputs-ing 到 outfile 之前用逗号将其内爆)规避了这种行为。

出于某种原因——对我来说并不明显——PHP 报告在 json_decode 调用期间内存使用量增加,这让我误以为 json_decode 函数是问题所在。

【讨论】:

  • 您介意提供更多详细信息吗?它可能会帮助我。您在循环的每次迭代中都重置了一个现有的字符串变量,但是用于保存旧字符串的内存没有被释放 - 这是问题所在吗?现在,使用数组来保存它确实释放内存的数据?
  • Scott,很抱歉没有回复您:我正在覆盖一个已经使用过的字符串 ($s = $s . "new contents";)。尽管众所周知,这样的连接会调用新的分配,但我不知道旧的副本会保留在原地并阻塞内存。所以我切换到像 $a = array(); 这样的方法array_push($a, "new contents";) 然后对数组进行内爆。
  • 仅供参考,这似乎也发生在对象上。在对象情况下,在设置新值之前对变量使用 unset 会阻止泄漏。
【解决方案4】:

有办法。

有一天我遇到了这个问题。我正在从 db 查询写入 csv 文件 - 总是分配一个 $row,然后在下一步重新分配它。一直内存不足。取消设置 $row 没有帮助;首先将 5MB 字符串放入 $row (以避免碎片)没有帮助;创建一个 $row-s 数组(将许多行加载到其中 + 在每 5000 步中取消整个设置)并没有帮助。 但这并不是结束,引用经典。

当我创建了一个单独的函数来打开文件,传输了 100.000 行(刚好不会占用整个内存)并关闭文件,然后我对该函数进行了后续调用(附加到现有文件),我发现对于每个函数退出,PHP 都会删除垃圾。这是一个局部变量空间的东西。

TL;DR

当函数退出时,它会释放所有局部变量。

如果您以较小的部分完成这项工作,例如在第一次函数调用中从 0 到 1000,然后从 1001 到 2000 等等,那么每次函数返回时,您的内存都会重新获得。垃圾收集很有可能在函数返回时发生。 (如果这是一个消耗大量内存的相对缓慢的函数,我们可以放心地假设它总是发生。)

旁注:对于通过引用传递的变量,它显然不起作用;函数只能释放其内部变量,这些变量在返回时会丢失。

我希望这能拯救你的一天,就像它拯救了我一样!

【讨论】:

  • 是的,我不知道我以前在哪里读过这个,但这个解决方案总是最适合我
  • 感谢您的提示!今天对我帮助很大。
  • @kalinma 总是很高兴 :) 让我担心的是,显然什么都没有改变……你使用的是 PHP7 吗?因为在如此强烈重构的引擎中遇到这个问题真的很可惜。
  • @dkellner,我实际上使用的是 PHP 5.6。由于 Joomla 3.7 支持它,升级到 7 会很好,但我们的服务器目前设置为 5.6。
  • 还有其他人在观看 Gotham 系列并且每次提到 GCPD 时都记得这篇文章吗?
【解决方案5】:

我发现 PHP 的内部内存管理器最有可能在函数完成时被调用。知道了这一点,我在这样的循环中重构了代码:

while (condition) {
  // do
  // cool
  // stuff
}

while (condition) {
  do_cool_stuff();
}

function do_cool_stuff() {
  // do
  // cool
  // stuff
}

编辑

我运行了这个快速基准测试并没有发现内存使用量增加。这让我相信泄漏不在json_decode()

for($x=0;$x<10000000;$x++)
{
  do_something_cool();
}

function do_something_cool() {
  $json = '{"a":1,"b":2,"c":3,"d":4,"e":5}';
  $result = json_decode($json);
  echo memory_get_peak_usage() . PHP_EOL;
}

【讨论】:

  • 这减少了泄漏,但并没有完全修复它......显然,json_decode 函数内部发生了泄漏 - 是否有任何替代实现?我不在乎它是否有点慢,只要它不占用内存(目前,程序在 60% 的处理中达到 1 GB 标记,导致机器交换并因此增长 VERY 慢...没有什么可以证明这样的内存使用是合理的,读取的块都是大约 10 MB 并且它们随后被处理)。
  • Mike,我尝试了同样的方法,但也无法使用“简单”方法(使用简单数组进行模糊测试)重现泄漏。将尝试使用我的输入数据运行它,也许这就是问题所在。
  • 在尝试重现整个过程时,我最终找到了解决方案:它是一个字符串连接。我通过连接一些变量(输出是 CSV 文件)逐行生成输入。然而,PHP 似乎没有释放用于字符串旧副本的内存,因此有效地用未使用的数据破坏了 RAM。切换到基于数组的方法(并在将其 fputs-ing 到 outfile 之前用逗号将其内爆)规避了这种行为。
  • @DBa:您能否为此创建一个答案并标记为正确?我花了我阅读所有的 cmets 来找到你的最终解决方案 :-P 这非常有帮助
  • 在函数中包装东西与“调用 GC”无关。发生的情况是,在函数结束时,该函数中使用的所有变量同时超出范围,就好像您同时拥有 unset() 它们一样。没有进行垃圾收集,变量只是达到 0 的引用计数并立即释放。
【解决方案6】:

在每个声明后致电memory_get_peak_usage(),并确保您尽一切可能unset()。如果您使用 foreach() 进行迭代,请使用引用变量以避免复制原始 (foreach())。

foreach( $x as &$y)

如果 PHP 确实在泄漏内存,则强制垃圾回收不会产生任何影响。

IBM 上有一篇关于 PHP 内存泄漏及其检测的好文章

【讨论】:

  • 使用 unset() 是一个很好的解决方案,但您仍然依赖 GC。您也可以尝试将不再需要的变量分配给 NULL。内存可能会被更快地回收。
  • IBM 文章基本上说“使用memory_get_peak_usage 定位泄漏,这不是很有帮助,因为我似乎已经找到了它 - 但是,我不知道如何摆脱内部 PHP 函数中的内存泄漏...
  • 如果它是 PHP 函数内部的,你无法摆脱它,这是语言中的错误!如果您检测到泄漏,也许您已经确定了一个函数,您应该 a) 尝试找到 b) 报告 @bugs.php.net 的等效函数@也许您应该发布您遇到问题的代码?
  • 那篇 IBM 文章是关于 PHP 5.2 的,当时 PHP 没有真正的垃圾收集器(即能够收集未引用循环的垃圾收集器)。如果您运行的是 PHP 5.3 或更高版本,请先尝试gc_collect_cycles(),因为可能会发生内存泄漏。
  • 1) 垃圾收集器补充 基于引用计数的释放,而不是替换它,所以unset() 在大多数情况下会立即释放内存。 2) 通过引用分配通常更差 的内存性能,因为它与 PHP 的自动 Copy-On-Write 优化冲突(正常的分配不会立即复制变量)。
【解决方案7】:

我要说的是,我不一定期望 gc_collect_cycles() 解决问题 - 因为大概文件不再映射到 zvars。但是您是否在加载任何文件之前检查了 gc_enable 是否被调用?

我注意到 PHP 在执行包含时似乎会占用内存 - 远远超过源文件和标记化文件所需的内存 - 这可能是一个类似的问题。我并不是说这是一个错误。

我相信一种解决方法是不使用 file_get_contents 而是使用 fopen()....fgets()...fclose() 而不是一次性将整个文件映射到内存中。但您需要尝试确认。

HTH

C.

【讨论】:

    【解决方案8】:

    最近有一个similar issueSystem_Daemon。今天我把我的问题隔离到file_get_contents

    您可以尝试改用fread 吗?我认为这可能会解决您的问题。 如果是这样,可能是时候在 PHP 上进行错误报告了。

    【讨论】:

      猜你喜欢
      • 2015-10-25
      • 2016-12-24
      • 2015-11-16
      • 1970-01-01
      • 1970-01-01
      • 2019-06-06
      • 2012-08-13
      • 1970-01-01
      • 2011-11-03
      相关资源
      最近更新 更多