Skip to content

引用计数基础

概述

PHP 使用引用计数(Reference Counting)作为主要的内存管理机制。每个 PHP 值(zval)内部都有一个引用计数器(refcount),记录有多少个变量名指向该值。当引用计数降为 0 时,PHP 自动释放该值所占用的内存。理解引用计数对于编写高效的 PHP 代码和避免内存泄漏至关重要。

核心机制

PHP 的内存管理基于"引用计数 + 循环引用收集器"的双重机制。引用计数处理大多数场景,循环引用收集器处理引用计数无法解决的特殊情况。

基础概念

zval 结构

zval(Zend Value)是 PHP 内部表示所有值的基本结构。每个变量都对应一个 zvalzval 包含值类型、值本身和引用计数。

引用计数的含义

引用计数记录了有多少个符号(变量名)指向同一个 zval。当引用计数为 0 时,该 zval 的内存被自动释放。

引用计数增减规则

  • 赋值给新变量:refcount + 1
  • 变量离开作用域(unset/函数返回):refcount - 1
  • refcount 变为 0:释放内存

语法与代码

基本引用计数

php
<?php
declare(strict_types=1);

// 创建变量:refcount = 1
$a = 'hello';
// $a 指向 zval("hello"), refcount = 1

// 赋值给新变量:refcount = 2(Copy-on-Write 共享)
$b = $a;
// $a 和 $b 共享同一个 zval("hello"), refcount = 2

// 修改 $b:Copy-on-Write 触发,$b 获得独立的 zval
$b = 'world';
// $a → zval("hello"), refcount = 1
// $b → zval("world"), refcount = 1

// unset $a:refcount - 1 = 0,"hello" 的内存被释放
unset($a);

函数参数的引用计数

php
<?php
declare(strict_types=1);

function process(array $data): array
{
    // $data 与外部变量共享 zval,refcount + 1
    // 但由于 Copy-on-Write,不会立即复制

    $data[] = 'new'; // 修改触发复制

    return $data;
}

$items = [1, 2, 3];
// refcount = 1

$result = process($items);
// 函数调用期间,$items 的 refcount 临时变为 2
// 但 process() 内部修改了 $data,触发了 Copy-on-Write
// $items 保持不变,refcount 恢复为 1

引用传递对引用计数的影响

php
<?php
declare(strict_types=1);

$x = 'data';
$y = &$x;
// $x 和 $y 通过引用绑定,is_ref = true

// 引用绑定下,Copy-on-Write 不会触发
$y = 'modified';
// $x 也变为 'modified'

// unset 断开引用
unset($y);
// $y 的 refcount 降为 0,但 "modified" 不被释放
// 因为 $x 仍然引用它

对象的引用计数

php
<?php
declare(strict_types=1);

class Document
{
    public function __construct(public string $title = '') {}
}

$doc = new Document('Test');
// 对象的 zval refcount = 1
// 对象本身有自己的引用计数

$copy = $doc;
// $doc 和 $copy 共享同一个对象标识符
// 对象的引用计数 + 1 = 2

unset($doc);
// 对象引用计数 - 1 = 1
// 对象不被销毁

$copy->title = 'Modified';
echo $copy->title; // Modified — 对象仍然存在

详细说明

Copy-on-Write 机制

Copy-on-Write(写时复制)是 PHP 引用计数的核心优化:

  1. 赋值时,两个变量共享同一个 zval(refcount 增加)
  2. 当其中一个变量被修改时,PHP 复制 zval,使两个变量各自独立
  3. 这样避免了不必要的内存复制

引用对 Copy-on-Write 的影响

当变量之间通过 & 建立引用关系时,Copy-on-Write 机制被禁用。这是因为引用的语义要求修改必须对所有引用变量可见。

引用计数的限制

引用计数无法处理循环引用(Circular Reference):

php
<?php
declare(strict_types=1);

$a = new stdClass();
$b = new stdClass();

$a->next = $b;
$b->next = $a;

unset($a);
unset($b);

// 此时 $a 和 $b 互相引用,各自的 refcount 不为 0
// 但没有任何外部变量引用它们
// 引用计数无法释放它们 → 内存泄漏!
// 需要循环引用收集器来处理

实战示例

实战:大数组操作中的引用计数

php
<?php
declare(strict_types=1);

function processLargeArray(array $data): array
{
    // 不修改 $data → 不触发 Copy-on-Write
    $total = array_sum($data);
    return ['sum' => $total, 'count' => count($data)];
}

$bigArray = range(1, 1000000);
// refcount = 1

// 传给函数时 refcount 临时变为 2
// 但函数不修改 $data,所以不会复制
$result = processLargeArray($bigArray);
// 函数返回后 refcount 恢复为 1

echo memory_get_usage() / 1024 / 1024 . " MB\n";

实战:引用计数与对象生命周期

php
<?php
declare(strict_types=1);

class Tracked
{
    public static int $created = 0;
    public static int $destroyed = 0;

    public function __construct()
    {
        self::$created++;
    }

    public function __destruct()
    {
        self::$destroyed++;
    }
}

$a = new Tracked();
$b = $a; // 对象引用计数 + 1

unset($a);
echo "销毁: " . Tracked::$destroyed . "\n"; // 0 — 对象仍然存在

unset($b);
echo "销毁: " . Tracked::$destroyed . "\n"; // 1 — 引用计数降为 0,触发析构

注意事项

引用计数与数组操作

php
<?php
declare(strict_types=1);

// 数组的引用计数
$shared = ['a', 'b', 'c'];
$ref1 = $shared;  // refcount = 2 (共享)
$ref2 = $shared;  // refcount = 3 (共享)

// 修改其中一个 — 触发 Copy-on-Write
$ref1[0] = 'X';
// 现在 $ref1 有自己的 zval, refcount = 1
// $shared 和 $ref2 仍然共享, refcount = 2

print_r($shared); // [a, b, c] — 未被修改
print_r($ref1);   // [X, b, c] — 已修改
print_r($ref2);   // [a, b, c] — 未被修改

引用计数与字符串

php
<?php
declare(strict_types=1);

// 字符串的 Copy-on-Write
$str1 = "Hello, World!";
$str2 = $str1; // refcount = 2 (共享)

// 修改 str2 — 触发复制
$str2 = "Hello, PHP!";
// str1 和 str2 现在各自独立

echo $str1; // Hello, World!
echo $str2; // Hello, PHP!

unset 的引用计数行为

php
<?php
declare(strict_types=1);

$a = "data";
$b = $a; // refcount = 2

unset($b); // $b 离开,refcount = 1
// $a 仍然指向 "data"

unset($a); // $a 离开,refcount = 0
// "data" 的内存被释放

实战:调试引用计数

php
<?php
declare(strict_types=1);

function demonstrateRefcount(): void
{
    $x = [1, 2, 3];
    echo "创建后内存: " . memory_get_usage() . "\n";

    $y = $x;
    echo "赋值后内存: " . memory_get_usage() . "\n";
    // 内存几乎不变 — Copy-on-Write 共享

    $y[] = 4;
    echo "修改后内存: " . memory_get_usage() . "\n";
    // 内存增加 — Copy-on-Write 触发复制

    unset($x);
    echo "unset 后内存: " . memory_get_usage() . "\n";
    // 内存可能减少(取决于引擎回收策略)
}

demonstrateRefcount();

debug_zval_dump 已弃用

PHP 8.2+

debug_zval_dump() 在 PHP 8.2 中被弃用。使用 debug_zval_refcount() 替代(PHP 8.2+)。

引用计数不是线程安全的

PHP 的引用计数机制在单线程模型下工作。在多线程环境(如 ZTS 构建)中,需要额外的锁机制。

最佳实践

  1. 信任 Copy-on-Write:不需要为了避免"复制"而使用引用传递。
  2. 避免不必要的引用:引用会禁用 Copy-on-Write。
  3. 关注循环引用:在涉及双向引用时注意内存泄漏。
  4. 使用 unset 释放:及时释放不再使用的大变量。
php
<?php
declare(strict_types=1);

// 推荐:值传递 + 返回新值
function processData(array $data): array
{
    $data[] = 'processed';
    return $data;
}

// 大数组处理完及时释放
$largeData = readLargeFile(...);
$result = transform($largeData);
unset($largeData); // 及时释放内存

深入分析:Copy-on-Write 的性能影响

数组赋值的性能测试

php
<?php
declare(strict_types=1);

function testCowPerformance(int $size): void
{
    $data = range(1, $size);
    $memBefore = memory_get_usage();

    // 值赋值(Copy-on-Write)
    $copy = $data;
    $memAfterAssign = memory_get_usage();
    echo "赋值后内存变化: " . ($memAfterAssign - $memBefore) . " bytes\n";
    // 几乎为 0 — Copy-on-Write 共享

    // 修改副本
    $copy[0] = -1;
    $memAfterModify = memory_get_usage();
    echo "修改后内存变化: " . ($memAfterModify - $memAfterAssign) . " bytes\n";
    // 显著增长 — 触发真正的复制
}

testCowPerformance(100000);

引用赋值的性能影响

php
<?php
declare(strict_types=1);

function testRefPerformance(int $iterations = 1000000): void
{
    $data = ['key' => 'value'];

    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $ref = &$data; // 引用赋值
    }
    $refTime = microtime(true) - $start;

    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $copy = $data; // 值赋值
    }
    $copyTime = microtime(true) - $start;

    echo "引用赋值: " . ($refTime * 1000) . "ms\n";
    echo "值赋值: " . ($copyTime * 1000) . "ms\n";
}

testRefPerformance();

对象引用计数 vs 值引用计数

php
<?php
declare(strict_types=1);

class Counter
{
    public static int $destructed = 0;

    public function __destruct()
    {
        self::$destructed++;
    }
}

function testObjectRefcount(): void
{
    Counter::$destructed = 0;

    $a = new Counter();
    $b = $a;
    $c = $a;
    // 对象引用计数 = 3

    unset($c);
    echo "unset c 后析构: " . Counter::$destructed . "\n"; // 0

    unset($b);
    echo "unset b 后析构: " . Counter::$destructed . "\n"; // 0

    unset($a);
    echo "unset a 后析构: " . Counter::$destructed . "\n"; // 1
}

testObjectRefcount();

参考链接