PHP如何管理内存以及为什么垃圾回收器不休息

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

为什么我们会在PHP中考虑内存问题?

\n

大多数PHP开发者习惯认为内存是无限的资源。请求一个包含百万个元素的数组?没问题。创建了一个对象,然后又忘了它?没关系,PHP会自动清理。但当你的项目开始因错误Allowed memory size exhausted而崩溃,或者莫名其妙地变慢时,就该深入了解底层到底发生了什么。

\n

今天,我们将深入探讨名为“PHP中的内存与垃圾回收器”的黑盒。你将了解PHP如何计算引用,为什么循环引用是祸害,以及如何让垃圾回收器为你所用,而不是与你作对。开始吧!

\n\n

PHP如何管理内存:从请求到响应

\n

与C或C++不同,你需要自己分配和释放内存,PHP通过Zend Engine工作。每个PHP进程都有自己的内存池。当你编写:

\n
$data = [1, 2, 3];
\n

PHP会为数组分配内存,保存值,并增加所谓的refcount(引用计数)。只要有人引用变量$data,内存就不会被释放。

\n

但当你执行:

\n
unset($data);
\n

引用计数降至零,Zend Engine会立即将内存返回给池。简单又快速。然而,还有一些细微之处……

\n\n

引用算术:refcount 和 is_ref

\n

PHP中的每个变量都是一个zval结构,它存储值、类型、is_ref标志(是否为引用)和refcount。让我们看一个例子:

\n
$a = "Hello";\n$b = $a;  // refcount 增加到2(写时复制)\n$b = "World"; // 现在 $a 和 $b 是不同的 zval,"Hello" 的 refcount 降至1\n
\n

这称为copy-on-write(写时复制)。只要你不改变值,PHP就会通过使用同一块内存来节省内存。但一旦你修改了变量,就会创建一个副本。很巧妙,对吧?

\n\n

垃圾回收器(GC):当 refcount 失效时

\n

普通的引用计数机制在99%的情况下都工作得很好。但有一个问题——循环引用。假设你有两个对象相互引用:

\n
class Node {\n    public $next;\n}\n\n$a = new Node();\n$b = new Node();\n$a->next = $b;\n$b->next = $a;\n\nunset($a);\nunset($b);\n
\n

在unset之后,两个对象的 refcount 仍然为1(每个都引用另一个)。从代码角度看,它们不可访问,但内存并未释放。这就是循环引用。

\n

如果这样的对象很多,内存就会泄漏。为了解决这个问题,从PHP 5.3版本开始,引入了完整的垃圾回收器(GC)。

\n\n

GC在PHP中如何工作?

\n

垃圾回收器不会在每一行代码上运行。它会在根缓冲区(潜在循环引用的数组)填满时激活。默认情况下,其大小为10,000个元素。当缓冲区满时,GC会运行一个算法,该算法:

\n
    \n
  • 扫描根——找到所有 refcount > 0 且可能成为循环一部分的 zval。
  • \n
  • 减少 refcount——对所有子元素(就像没有引用一样)。
  • \n
  • 检查——如果减少后 refcount 变为零,则说明是“垃圾”,可以删除。
  • \n
  • 恢复——对不是垃圾的元素恢复 refcount,并释放内存。
  • \n
\n

这个过程称为garbage collection。你可以强制运行它:

\n
gc_collect_cycles();
\n

但不要频繁调用——这是一个开销很大的操作。

\n\n

如何追踪PHP中的内存泄漏?

\n

如果你的脚本运行时间很长(例如,队列工作者或守护进程),内存泄漏就会变得至关重要。以下是一些工具

博客

书籍推荐