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() 现在返回 nullWeakReference 类
php
<?php
declare(strict_types=1);
// WeakReference 只有两个方法:
// - WeakReference::create(object $object): WeakReference
// - WeakReference::get(): ?objectWeakMap 类
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"; // 0WeakMap 实现对象元数据缓存
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'; // 应该是 nullGC 时机
弱引用所引用的对象在 unset() 后不会立即回收,需要等待 GC 运行。PHP 会在内存达到阈值时自动触发 GC,也可以手动调用 gc_collect_cycles()。在大多数应用中,不需要手动触发 GC。
最佳实践
- 使用 WeakMap 替代 WeakReference+数组:WeakMap 是更高级的抽象,自动清理条目
- ORM/实体管理使用 WeakMap:避免对象身份映射导致的内存泄漏
- 事件系统使用 WeakMap:自动清理已销毁组件的事件监听器
- 元数据附加使用 WeakMap:为对象添加临时元数据,不影响对象生命周期
- 不要过度使用弱引用:只在确实需要避免循环引用或内存泄漏时使用
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;
}
}