为什么我们会在PHP中考虑内存问题?
\n大多数PHP开发者习惯认为内存是无限的资源。请求一个包含百万个元素的数组?没问题。创建了一个对象,然后又忘了它?没关系,PHP会自动清理。但当你的项目开始因错误Allowed memory size exhausted而崩溃,或者莫名其妙地变慢时,就该深入了解底层到底发生了什么。
今天,我们将深入探讨名为“PHP中的内存与垃圾回收器”的黑盒。你将了解PHP如何计算引用,为什么循环引用是祸害,以及如何让垃圾回收器为你所用,而不是与你作对。开始吧!
\n\nPHP如何管理内存:从请求到响应
\n与C或C++不同,你需要自己分配和释放内存,PHP通过Zend Engine工作。每个PHP进程都有自己的内存池。当你编写:
\n$data = [1, 2, 3];\nPHP会为数组分配内存,保存值,并增加所谓的refcount(引用计数)。只要有人引用变量$data,内存就不会被释放。
\n但当你执行:
\nunset($data);\n引用计数降至零,Zend Engine会立即将内存返回给池。简单又快速。然而,还有一些细微之处……
\n\n引用算术:refcount 和 is_ref
\nPHP中的每个变量都是一个zval结构,它存储值、类型、is_ref标志(是否为引用)和refcount。让我们看一个例子:
$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%的情况下都工作得很好。但有一个问题——循环引用。假设你有两个对象相互引用:
\nclass 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(每个都引用另一个)。从代码角度看,它们不可访问,但内存并未释放。这就是循环引用。
如果这样的对象很多,内存就会泄漏。为了解决这个问题,从PHP 5.3版本开始,引入了完整的垃圾回收器(GC)。
\n\nGC在PHP中如何工作?
\n垃圾回收器不会在每一行代码上运行。它会在根缓冲区(潜在循环引用的数组)填满时激活。默认情况下,其大小为10,000个元素。当缓冲区满时,GC会运行一个算法,该算法:
\n- \n
- 扫描根——找到所有 refcount > 0 且可能成为循环一部分的 zval。 \n
- 减少 refcount——对所有子元素(就像没有引用一样)。 \n
- 检查——如果减少后 refcount 变为零,则说明是“垃圾”,可以删除。 \n
- 恢复——对不是垃圾的元素恢复 refcount,并释放内存。 \n
这个过程称为garbage collection。你可以强制运行它:
\ngc_collect_cycles();\n但不要频繁调用——这是一个开销很大的操作。
\n\n如何追踪PHP中的内存泄漏?
\n如果你的脚本运行时间很长(例如,队列工作者或守护进程),内存泄漏就会变得至关重要。以下是一些工具