PHP 作为一门解释型脚本语言,常被误解为“内存管理很随意”。然而,在面试中高级 PHP 岗位时,内存管理、引用计数与写时复制(Copy-On-Write)几乎是必考内容。理解这些机制,不仅能帮你应对面试,更能写出高性能、低内存的代码。本文从面试官视角出发,逐层拆解这些核心概念。
一、PHP 内存管理的基本架构
PHP 的内存管理分为两层:
-
Zend 内存管理器(Zend Memory Manager,简称 ZendMM)
负责从操作系统申请大块内存,再按需分配给 PHP 变量、数组、对象等。它维护内存池,减少频繁的系统调用(如malloc/free),提升性能。 -
垃圾回收机制(Garbage Collector,GC)
专门处理“循环引用”导致的内存泄漏问题。PHP 5.3 之前没有 GC,循环引用会一直占用内存;5.3 之后引入了基于引用计数 + 同步周期回收的算法。
面试常问:PHP 中变量什么时候释放?
答:当变量的引用计数减为 0 时,ZendMM 会立即回收其占用的内存。但循环引用时,引用计数永远不为 0,需要 GC 介入。
二、引用计数(Reference Counting)
每个 PHP 变量在底层都对应一个 zval 结构。zval 中有一个 refcount__gc 字段,记录有多少个变量名(或符号表条目)指向同一个值。
2.1 基本规则
- 赋值(
$a = $b)时,refcount加 1。 - 销毁变量(
unset($a))或变量离开作用域时,refcount减 1。 - 当
refcount变为 0,内存被释放。
2.2 示例
$a = "hello"; // refcount = 1
$b = $a; // refcount = 2
unset($a); // refcount = 1,$b 仍然有效
2.3 面试陷阱:引用赋值 &
$a = "hello";
$b = &$a; // 引用赋值,refcount 仍为 1,但 is_ref = 1
$b = "world"; // $a 也变成 "world"
此时 zval 的 is_ref 标志位为 1,表示这是一个引用变量。引用赋值不会增加 refcount,但会改变变量的行为。
常见问题:unset($a) 后 $b 还有值吗?
答:有。因为 unset 只是断开变量名与 zval 的关联,refcount 减 1,只要 refcount > 0,值就保留。
三、写时复制(Copy-On-Write)
写时复制是 PHP 优化内存的核心手段。当多个变量指向同一个 zval 时,只要没有发生“写”操作,它们就共享同一份内存。一旦某个变量尝试修改,Zend 引擎才会复制一份新的 zval,并修改其 refcount。
3.1 触发条件
- 变量不是引用(
is_ref = 0) refcount > 1- 发生写操作(赋值、修改数组元素、修改对象属性等)
3.2 示例
$a = str_repeat("x", 1000000); // 大字符串
$b = $a; // 不复制,refcount = 2
$b .= "y"; // 触发写时复制,$b 独立,$a 不变
3.3 数组的写时复制
$arr1 = [1, 2, 3];
$arr2 = $arr1; // 共享,refcount = 2
$arr2[] = 4; // 触发 COW,$arr1 仍为 [1,2,3]
面试高频题:$arr2 = $arr1; $arr2[0] = 100; 会复制整个数组吗?
答:会。PHP 的数组 COW 是“整体复制”,不是只复制被修改的元素。因此大数组的写操作代价较高。
3.4 引用与 COW 的冲突
$a = [1, 2, 3];
$b = &$a; // 引用,is_ref = 1
$b[] = 4; // 不会触发 COW,直接修改原数组
一旦变量被标记为引用,COW 机制失效,所有引用变量共享同一份数据。
四、循环引用与垃圾回收
引用计数无法处理循环引用:
class Node {
public $next;
}
$a = new Node();
$b = new Node();
$a->next = $b;
$b->next = $a;
unset($a, $b); // refcount 不为 0,内存泄漏
PHP 的 GC 采用 同步周期回收算法:
- 当根缓冲区(root buffer)满(默认 10000 个可能根)时,触发 GC。
- 标记所有可能根为紫色。
- 遍历,将引用计数减 1,标记为灰色。
- 扫描灰色节点,若引用计数为 0,标记为白色(可回收);否则标记为黑色。
- 回收白色节点。
面试常问:
如何手动触发 GC?
答:gc_collect_cycles()。
如何关闭/开启 GC?
答:gc_disable() / gc_enable()。
五、实战面试题精选
Q1:$a = 1; $b = $a; unset($a); 此时内存中 zval 的 refcount 是多少?
答:$b 仍指向该 zval,refcount 为 1。
Q2:为什么 PHP 数组的 COW 是整体复制?
答:因为数组在底层是 HashTable,元素存储不连续,单独复制某个元素无法保证内存安全与一致性。整体复制实现简单且不易出错。
Q3:如何检测 PHP 内存泄漏?
答:使用 memory_get_usage() 观察内存增长;用 gc_status() 查看 GC 统计;或用 Xdebug、Valgrind 分析。
Q4:unset() 和 $var = null 的区别?
答:unset() 断开变量名与 zval 的关联,refcount 减 1;$var = null 会新建一个 null 的 zval,原 zval refcount 减 1。两者最终都可能释放内存,但 unset 更彻底。
六、总结
| 机制 | 作用 | 关键点 |
|---|---|---|
| 引用计数 | 实时回收无引用变量 | refcount 为 0 时释放 |
| 写时复制 | 延迟复制,节省内存 | 读共享,写复制 |
| 垃圾回收 | 处理循环引用 | 根缓冲区 + 同步周期回收 |
理解这三者,不仅能回答面试题,更能指导你写出高效代码:避免不必要的引用、慎用大数组赋值、及时 unset 不再使用的变量。PHP 的内存管理并不神秘,它是一套精巧的工程权衡。掌握它,你就掌握了 PHP 性能优化的第一把钥匙。
未经允许不得转载:任鹏个人博客 » PHP 面试精讲:PHP 内存管理、引用计数与写时复制机制详解

