Skip to content

WeakReference / WeakMap 接口

概述

PHP 7.4 引入了 WeakReference 类,PHP 8.0 引入了 WeakMap 类。它们提供了一种"弱引用"机制:所引用的对象不会阻止垃圾回收器(GC)回收该对象。

弱引用的核心应用场景是缓存关联元数据。当对象不再被其他强引用持有时,即使弱引用仍然存在,GC 也会回收该对象,从而避免内存泄漏。

  • WeakReference — 对单个对象的弱引用,通过 get() 获取原始对象(可能已回收返回 null)
  • WeakMap — 弱引用映射表,键为对象(弱引用),值为任意数据。键对象被回收后,映射条目自动删除

版本说明

  • WeakReference 从 PHP 7.4.0 起可用
  • WeakMap 从 PHP 8.0.0 起可用
  • PHP 8.1+ 中 WeakMap 使用更广泛,常用于框架的依赖注入容器和 ORM 的对象身份映射

基础概念

什么是弱引用

普通引用(强引用)会阻止 GC 回收对象。弱引用不会。

php
<?php

declare(strict_types=1);

// 强引用 — 对象不会被回收
$object = new stdClass();
$ref = $object;  // 强引用,$object 和 $ref 都指向同一个对象
unset($object);
// 对象仍然被 $ref 引用,不会被 GC 回收

// 弱引用 — 不阻止 GC
$object2 = new stdClass();
$weakRef = WeakReference::create($object2);
unset($object2);
// 对象没有强引用了,GC 可以回收
// $weakRef->get() 现在返回 null

WeakReference 类

php
<?php

declare(strict_types=1);

// WeakReference 只有两个方法:
// - WeakReference::create(object $object): WeakReference
// - WeakReference::get(): ?object

WeakMap 类

php
<?php

declare(strict_types=1);

// WeakMap 实现 Countable 和 IteratorAggregate
// interface WeakMap implements Countable, IteratorAggregate
// {
//     public function offsetGet(object $object): mixed;
//     public function offsetSet(object $object, mixed $value): void;
//     public function offsetExists(object $object): bool;
//     public function offsetUnset(object $object): void;
//     public function count(): int;
//     public function getIterator(): Traversable;
// }

语法与代码

WeakReference 基本用法

php
<?php

declare(strict_types=1);

class User
{
    public function __construct(private readonly string $name)
    {
        echo "用户 {$this->name} 被创建\n";
    }

    public function __destruct()
    {
        echo "用户 {$this->name} 被销毁\n";
    }

    public function name(): string
    {
        return $this->name;
    }
}

$user = new User('Alice');
$weakRef = WeakReference::create($user);

echo $weakRef->get()?->name();  // Alice

unset($user);
// 此时如果触发 GC,User 对象可能被回收
gc_collect_cycles();

echo $weakRef->get()?->name() ?? 'null';  // null(对象已被回收)

WeakMap 基本用法

php
<?php

declare(strict_types=1);

class DbConnection
{
    public function __construct(private readonly string $dsn)
    {
        echo "数据库连接 {$this->dsn} 已建立\n";
    }

    public function __destruct()
    {
        echo "数据库连接 {$this->dsn} 已关闭\n";
    }
}

// WeakMap:用对象作为键存储关联数据
$connectionStats = new WeakMap();

function getConnection(string $dsn): DbConnection
{
    static $cache;
    $cache ??= new WeakMap();

    // 缓存连接对象
    // 每个连接对象关联统计信息
    $conn = new DbConnection($dsn);
    $cache[$conn] = [
        'queries' => 0,
        'created_at' => time(),
    ];

    return $conn;
}

$conn = getConnection('mysql:host=localhost');
$connectionStats[$conn] = ['status' => 'active'];

unset($conn);
gc_collect_cycles();
// DbConnection 被销毁,WeakMap 中的条目自动移除
echo count($connectionStats);  // 0

详细说明

WeakMap 的自动清理机制

php
<?php

declare(strict_types=1);

$map = new WeakMap();

class Entity
{
    public function __construct(public readonly string $id)
    {
        echo "创建 Entity({$this->id})\n";
    }

    public function __destruct()
    {
        echo "销毁 Entity({$this->id})\n";
    }
}

$e1 = new Entity('1');
$e2 = new Entity('2');

$map[$e1] = ['label' => '第一个实体'];
$map[$e2] = ['label' => '第二个实体'];

echo "WeakMap 大小: " . count($map) . "\n";  // 2

// 释放一个实体的强引用
unset($e1);
gc_collect_cycles();

echo "WeakMap 大小: " . count($map) . "\n";  // 1($e1 的条目自动移除)

// 释放另一个
unset($e2);
gc_collect_cycles();

echo "WeakMap 大小: " . count($map) . "\n";  // 0

WeakMap 实现对象元数据缓存

php
<?php

declare(strict_types=1);

class MetadataCache
{
    private WeakMap $cache;

    public function __construct()
    {
        $this->cache = new WeakMap();
    }

    /**
     * 为对象附加元数据
     */
    public function setMetadata(object $object, string $key, mixed $value): void
    {
        if (!isset($this->cache[$object])) {
            $this->cache[$object] = [];
        }
        $this->cache[$object][$key] = $value;
    }

    /**
     * 获取对象的元数据
     */
    public function getMetadata(object $object, string $key, mixed $default = null): mixed
    {
        return $this->cache[$object][$key] ?? $default;
    }

    /**
     * 检查对象是否有元数据
     */
    public function hasMetadata(object $object, string $key): bool
    {
        return isset($this->cache[$object][$key]);
    }
}

$cache = new MetadataCache();

class Product
{
    public function __construct(public readonly string $sku)
    {}
}

$product = new Product('SKU-001');
$cache->setMetadata($product, 'viewCount', 42);
$cache->setMetadata($product, 'lastViewed', '2025-01-15');

echo $cache->getMetadata($product, 'viewCount');  // 42

// 当 $product 不再被引用时,关联的元数据自动释放

WeakMap 实现 ORM 对象身份映射

php
<?php

declare(strict_types=1);

class IdentityMap
{
    private WeakMap $map;

    public function __construct()
    {
        $this->map = new WeakMap();
    }

    /**
     * 根据主键查找或创建实体
     */
    public function getOrCreate(string $className, string $id, callable $factory): object
    {
        // 先在弱引用缓存中查找
        foreach ($this->map as $existing => $data) {
            if ($data['class'] === $className && $data['id'] === $id) {
                return $existing;
            }
        }

        // 不存在则创建
        $entity = $factory($id);
        $this->map[$entity] = [
            'class' => $className,
            'id'    => $id,
        ];

        return $entity;
    }

    public function size(): int
    {
        return count($this->map);
    }
}

// 用法
$identityMap = new IdentityMap();

$user1 = $identityMap->getOrCreate(
    User::class,
    'u1',
    fn (string $id) => new User($id)
);

$user2 = $identityMap->getOrCreate(
    User::class,
    'u1',
    fn (string $id) => new User($id)  // 不会再次调用
);

// $user1 和 $user2 是同一个对象(如果 $user1 仍存活)

实战示例

使用 WeakMap 实现事件监听器自动清理

php
<?php

declare(strict_types=1);

class EventEmitter
{
    /** @var WeakMap<object, array<string, callable>> */
    private WeakMap $listeners;

    public function __construct()
    {
        $this->listeners = new WeakMap();
    }

    public function on(object $owner, string $event, callable $callback): void
    {
        if (!isset($this->listeners[$owner])) {
            $this->listeners[$owner] = [];
        }
        $this->listeners[$owner][$event] = $callback;
    }

    public function emit(object $owner, string $event, mixed ...$args): void
    {
        if (isset($this->listeners[$owner][$event])) {
            ($this->listeners[$owner][$event])(...$args);
        }
    }

    public function hasListeners(object $owner): bool
    {
        return isset($this->listeners[$owner]) && !empty($this->listeners[$owner]);
    }
}

// 用法
class Component
{
    public function __construct(public readonly string $name)
    {}
}

$emitter = new EventEmitter();

$button = new Component('SubmitButton');
$emitter->on($button, 'click', fn () => echo "按钮 {$button->name} 被点击\n"));

$emitter->emit($button, 'click');  // 按钮 SubmitButton 被点击

// 当 $button 被销毁时,事件监听器自动清理

使用 WeakReference 实现观察者模式

php
<?php

declare(strict_types=1);

class WeakObserver
{
    /** @var array<int, WeakReference> */
    private array $observers = [];

    /** @var array<int, string> */
    private array $methods = [];

    public function attach(object $observer, string $method): void
    {
        $this->observers[] = WeakReference::create($observer);
        $this->methods[] = $method;
    }

    public function notify(string $event, mixed $data = null): void
    {
        foreach ($this->observers as $index => $weakRef) {
            $observer = $weakRef->get();
            if ($observer === null) {
                // 观察者已被回收,移除
                unset($this->observers[$index], $this->methods[$index]);
                continue;
            }
            $method = $this->methods[$index];
            $observer->$method($event, $data);
        }
    }

    public function cleanDead(): int
    {
        $before = count($this->observers);
        $this->observers = array_filter(
            $this->observers,
            fn (WeakReference $ref): bool => $ref->get() !== null
        );
        $this->methods = array_slice($this->methods, 0, count($this->observers));
        return $before - count($this->observers);
    }
}

注意事项

WeakMap 的键必须是对象

php
<?php

declare(strict_types=1);

$map = new WeakMap();

$map[new stdClass()] = 'value';       // 正确
// $map['string'] = 'value';         // TypeError
// $map[42] = 'value';                // TypeError
// $map[null] = 'value';             // TypeError

// WeakMap 的键类型:object(包括匿名类、闭包等)
// WeakMap 的值类型:mixed(任意类型)

WeakMap 不能使用 ArrayAccess 的 null offset

php
<?php

declare(strict_types=1);

$map = new WeakMap();

// WeakMap::offsetSet 的第一个参数必须是对象
// 不支持 $map[] = 'value'(没有键的追加)
// 不支持 null 作为键

// WeakMap 与普通数组/ArrayObject 的区别:
// 1. 键必须是对象
// 2. 键是弱引用(不阻止 GC)
// 3. 键被回收后条目自动删除
// 4. 不能用 isset() 检查(用 offsetExists)

GC 触发时机

php
<?php

declare(strict_types=1);

// GC 不是立即执行的
// 调用 gc_collect_cycles() 可以手动触发
// 但一般情况下 PHP 会自动触发 GC

$obj = new stdClass();
$weak = WeakReference::create($obj);
unset($obj);

// 此时 $weak->get() 可能仍然返回对象
// 因为 GC 可能还没执行
echo $weak->get() === null ? 'null' : 'object';  // 可能是 object

gc_collect_cycles();
// 手动触发 GC 后
echo $weak->get() === null ? 'null' : 'object';  // 应该是 null

GC 时机

弱引用所引用的对象在 unset() 后不会立即回收,需要等待 GC 运行。PHP 会在内存达到阈值时自动触发 GC,也可以手动调用 gc_collect_cycles()。在大多数应用中,不需要手动触发 GC。

最佳实践

  1. 使用 WeakMap 替代 WeakReference+数组:WeakMap 是更高级的抽象,自动清理条目
  2. ORM/实体管理使用 WeakMap:避免对象身份映射导致的内存泄漏
  3. 事件系统使用 WeakMap:自动清理已销毁组件的事件监听器
  4. 元数据附加使用 WeakMap:为对象添加临时元数据,不影响对象生命周期
  5. 不要过度使用弱引用:只在确实需要避免循环引用或内存泄漏时使用
php
<?php

declare(strict_types=1);

// 最佳实践:框架中的 DI 容器使用 WeakMap 缓存
class Container
{
    private WeakMap $resolved;

    public function __construct()
    {
        $this->resolved = new WeakMap();
    }

    public function resolve(string $className, callable $factory): object
    {
        // 检查是否已解析过(避免重复创建)
        // 注意:WeakMap 不支持字符串键,这里需要结合其他方案
        $instance = $factory();
        return $instance;
    }
}

参考链接