PHP 的垃圾回收机制,一直是很多开发者感兴趣的话题。特别是从 PHP 5 升级到 PHP 7 之后,变量底层实现和回收策略都发生了不小的变化。今天,我们就来系统地梳理一下,这两个版本在变量存储和垃圾回收上到底有什么不同。
变量的实现
PHP 的变量是弱类型的,一个变量可以表示整数、浮点数、字符串,甚至是数组或对象。这种灵活性,其实得益于底层统一用了一个叫 zval 的结构体来承载。
PHP 5.* 中的 zval 和 zend_value
在 PHP 5 时代,zval 的结构大致是这样的:
struct _zval_struct {
zvalue_value value;
zend_uint refcount__gc;
zend_uchar type;
zend_uchar is_ref__gc;
}
typedef union _zvalue_value {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
zend_ast *ast;
} zvalue_value;
简单来说,zval 里包含了一个联合体 zvalue_value 来存储实际的值,以及引用计数、类型标记和是否被引用的标记。引用计数和 is_ref 是直接放在 zval 结构体里的。
PHP 7.0 中的 zval 和 zend_value
到了 PHP 7,zval 的结构被大幅重构,变得更紧凑、更高效:
struct _zval_struct {
union {
zend_long lval;
double dval;
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} value;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type,
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved)
} v;
uint32_t type_info;
} u1;
union {
uint32_t var_flags;
uint32_t next;
uint32_t cache_slot;
uint32_t lineno;
uint32_t num_args;
uint32_t fe_pos;
uint32_t fe_iter_idx;
} u2;
};
可以看到,PHP 7 的 zval 不再直接存储引用计数了,而是把它放到了具体的 value 结构体中。同时,zval 本身不再处理写时复制(写时分离),而是通过一个专门的 zend_reference 结构体来表示引用关系。
PHP5 与 PHP7 引用计数的对比
在 PHP 5 中,变量赋值、函数传参等操作,都会直接操作 zval 里的 refcount__gc。当出现循环引用,比如两个数组互相引用,并且外部变量被 unset 之后,就会形成典型的“垃圾”。

而 PHP 7 的做法是,把引用计数放到具体的 value 类型中(比如 zend_string、zend_array),zval 本身不再负责计数。这带来的好处是,zval 可以更轻量地传递,写时复制(写时分离)的机制也被简化了。

理解了这些底层存储细节,我们才能真正理解 PHP 是如何做垃圾回收的。
什么是垃圾
首先,得明确什么才算“垃圾”。
- 引用计数增加的情况,肯定不是垃圾。
- 引用计数降到 0 的变量,也不是垃圾——它会被立即回收。
- 真正需要关注的,是那些引用计数减少,但减少后不等于 0 的变量。这些变量很可能构成了循环引用,成了无法被正常回收的“垃圾”。
垃圾收集
在 PHP 7 中,垃圾收集器只处理两种类型:数组和对象。并且,只有满足以下条件时,才会被送入收集流程:
- 变量的
type_flag标记为IS_TYPE_COLLECTABLE。 - 它还没有在垃圾缓冲区中存在过。
- 它没有被标记过。
- 满足条件后,会被标记为紫色,并放入缓冲区。
回收算法
从 PHP 5.3 开始,垃圾回收算法就已经基本定型了。它的核心思路可以概括为以下几个步骤:
- 把疑似垃圾的节点放入一个 root 池中。
- 当 root 池中的节点数量达到 10000 个时,就会触发一次垃圾回收。
- 遍历这个双向链表中的所有节点,把它们的引用计数减 1(模拟断开所有引用)。
- 再次遍历,将引用计数为 0 的节点删除,放入 free 队列。
- 对于引用计数不为 0 的节点,把引用计数加 1,恢复原状。

这套算法的巧妙之处在于,通过模拟减引用,可以快速识别出那些真正被循环引用“困住”的垃圾,而不会误伤正常的变量。