PHP在长时间运行的脚本中何时运行垃圾回收?

6

我正在编写一个 PHP cli 程序,用作队列系统的工作进程。

我原以为在这种情况下,PHP 会定期回收垃圾,不会一直达到内存限制。

但事实并非如此。

注意事项

  • 运行在 PHP 7 上
  • 这是一个长时间运行的脚本
  • zend.enable_gc = 1
  • 没有全局变量
1个回答

8

问题就在于这里。你必须手动调用gc_collect_cycles()来触发垃圾回收。

我写了一堆代码来尝试追踪,最后只剩下两个脚本:

这个不会崩溃:

for($i = 0;$i < 100;$i++) {
    useMemory();
    gc_collect_cycles();
}

这个程序会崩溃:

for($i = 0;$i < 100;$i++) {
    useMemory();
}

这是一个比较这些脚本的链接:Blackfire
可以看到,当你不调用gc_collect_cycles时,垃圾回收就不会发生,你会达到内存限制,PHP会自己终止。
PHP甚至没有利用这个机会来进行垃圾回收。关于这个问题的原因在PHP-DEV邮件列表上有讨论,但基本上归结为当内存限制已达到时如何运行需要内存的__destruct方法的复杂性。(还有在错误跟踪器#60982上)。
内存使用函数:
以下是我用来“浪费”内存的代码,它故意创建只能通过垃圾回收清除的循环引用。请注意,如果没有这些循环,对象将在超出范围后立即通过引用计数清除。
class Big {
    private $data;
    public function __construct($d = 0) {
        for($i = 0;$i< 1024 * 10;$i++) {
            $this->$i = chr(rand(97, 122));
        }
    }
}

function useMemory() {
    $a = new Big();
    $b = new Big();

    $a->b = $b;
    $b->a = $a;
}

你需要在这里调用 gc_collect_cyles() 的原因是因为垃圾回收器在其循环收集算法中使用的根缓冲区最大值为10,000,而内存限制已经达到。每个 Big 对象在我的系统上几乎占用 1 MB 的空间,每次迭代会添加 2 个根($a 和 $b)。在第 64 次迭代时,将存在 128 个根,并且将超过 128 MB 的内存限制。如果每个对象小了100倍,那么就可以“容纳” 10,000 个对象在 128 MB 的限制内,垃圾回收器会为您清除它们。但是,具有循环关系的大型对象可能需要手动调用 gc_collect_cyles() - molecularbear
@mcfedr 我正在考虑使用PHP程序作为RabbitMQ的工作者/消费者进程。如果您能分享一下您的经验,我将不胜感激。它已经运行了多长时间而没有任何问题?您是否使用systemd或其他方法来保持其运行? - MyO
@MyO 是的,我基本上发现你需要一些东西来保持它运行,我大多数时候使用 supervisord,但 systemd 也可以做到同样的事情 - 我还添加了一些代码,以便在从队列中获取作业之间,检查至少有 X% 的内存剩余,并杀死进程 - 麻烦的是没有编写为内存安全的 php 库,随着系统的增长,很难控制一切。顺便说一句,我真的推荐 Symfony Messenger 组件,它可以为您处理许多这些事情。https://symfony.com/doc/current/messenger.html - mcfedr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接