性能考量
概述
PHP 的垃圾回收机制(引用计数 + 循环引用收集器)在大多数场景下运行良好,但在某些特殊情况下可能影响性能。循环引用不仅会导致内存泄漏,还会增加 GC 的运行开销。本章将探讨循环引用对性能的影响、WeakReference/WeakMap 替代方案,以及避免循环引用的最佳实践。
核心观点
大多数 PHP 脚本是短生命周期的(请求-响应模式),GC 的影响微乎其微。但长运行脚本(守护进程、队列消费者)需要特别关注 GC 性能。
基础概念
循环引用对性能的影响
循环引用导致:
- 内存无法及时释放
- GC 运行时占用 CPU 时间
- root buffer 满时触发大规模扫描
WeakReference / WeakMap
PHP 8.0 引入 WeakReference,PHP 8.0 引入 WeakMap。它们提供了一种"不增加引用计数"的引用方式,可以避免循环引用。
避免循环引用
在可能的情况下,通过调整数据结构避免双向引用。
语法与代码
WeakReference
php
<?php
declare(strict_types=1);
class CacheEntry
{
public function __construct(public readonly string $key, public readonly mixed $value) {}
}
$obj = new CacheEntry('user:1', ['name' => 'Alice']);
$weakRef = \WeakReference::create($obj);
echo $weakRef->get()?->key . "\n"; // user:1
// 对象被销毁
unset($obj);
echo $weakRef->get() . "\n"; // null — 弱引用不阻止回收WeakMap
php
<?php
declare(strict_types=1);
class MetadataCache
{
private \WeakMap $cache;
public function __construct()
{
$this->cache = new \WeakMap();
}
public function set(object $obj, array $metadata): void
{
$this->cache[$obj] = $metadata;
}
public function get(object $obj): ?array
{
return $this->cache[$obj] ?? null;
}
}
$cache = new MetadataCache();
$user = new stdClass();
$user->name = 'Alice';
$cache->set($user, ['created_at' => '2024-01-01', 'source' => 'api']);
print_r($cache->get($user));
unset($user);
// $user 被回收,WeakMap 中对应的条目自动删除
echo $cache->get(new stdClass()) ?? 'null'; // null避免循环引用:使用 ID 替代对象引用
php
<?php
declare(strict_types=1);
// 不推荐:循环引用
class TreeNodeBad
{
public ?TreeNodeBad $parent = null;
/** @var array<TreeNodeBad> */
public array $children = [];
}
// 推荐:使用 ID 替代对象引用
class TreeNodeGood
{
public ?int $parentId = null;
/** @var array<int> */
public array $childIds = [];
private array $nodes = [];
public function addChild(int $id): void
{
$this->childIds[] = $id;
}
public function getChild(int $id): ?self
{
return $this->nodes[$id] ?? null;
}
}使用 WeakMap 实现观察者模式
php
<?php
declare(strict_types=1);
class EventDispatcher
{
private \WeakMap $listeners;
public function __construct()
{
$this->listeners = new \WeakMap();
}
public function addListener(object $listener, callable $handler): void
{
$this->listeners[$listener] = $handler;
}
public function dispatch(object $event): void
{
foreach ($this->listeners as $listener => $handler) {
$handler($event);
}
}
}详细说明
PHP 请求-响应模型的优势
PHP 的典型运行模式是每个请求一个进程/线程。请求结束后,所有内存被释放,因此循环引用在短生命周期脚本中不是问题。
长运行脚本中的 GC 影响
| 场景 | GC 影响 |
|---|---|
| Web 请求(< 1s) | 几乎无影响 |
| CLI 脚本(< 30s) | 影响很小 |
| 守护进程(运行数小时) | 需要关注 |
| 队列消费者(运行数小时) | 需要关注 |
WeakReference vs WeakMap
| 特性 | WeakReference | WeakMap |
|---|---|---|
| PHP 版本 | 8.0+ | 8.0+ |
| 存储 | 单个引用 | 键值对映射 |
| 查找 | 需要保存引用 | 自动管理 |
| 适用场景 | 单一对象关联 | 缓存、元数据 |
实战示例
实战:WeakMap 实现 ORM 关系缓存
php
<?php
declare(strict_types=1);
class OrmCache
{
private \WeakMap $relatedCache;
public function __construct()
{
$this->relatedCache = new \WeakMap();
}
public function setRelated(object $entity, string $relation, mixed $value): void
{
if (!isset($this->relatedCache[$entity])) {
$this->relatedCache[$entity] = [];
}
$this->relatedCache[$entity][$relation] = $value;
}
public function getRelated(object $entity, string $relation): mixed
{
return $this->relatedCache[$entity][$relation] ?? null;
}
}
// 使用 WeakMap 缓存关联数据,当实体被回收时缓存自动清除
$cache = new OrmCache();
$user = new stdClass();
$user->id = 1;
$cache->setRelated($user, 'posts', [{'title' => 'Post 1'}]);
$cache->setRelated($user, 'comments', [{'text' => 'Nice!'}]);
print_r($cache->getRelated($user, 'posts'));
// 当 $user 被回收后,WeakMap 中的缓存自动清除实战:使用事件监听替代双向引用
php
<?php
declare(strict_types=1);
class EventEmitter
{
/** @var array<string, array<callable>> */
private array $listeners = [];
public function on(string $event, callable $listener): void
{
$this->listeners[$event][] = $listener;
}
public function emit(string $event, mixed ...$data): void
{
foreach ($this->listeners[$event] ?? [] as $listener) {
$listener(...$data);
}
}
}
// 使用事件替代 parent 引用
class ChildNode
{
public function __construct(
public readonly string $name,
private readonly EventEmitter $emitter
) {}
public function notifyParent(): void
{
$this->emitter->emit('child:changed', $this->name);
}
}
$emitter = new EventEmitter();
$emitter->on('child:changed', function (string $name): void {
echo "父节点收到通知: {$name} 变更\n";
});
$child = new ChildNode("child1", $emitter);
$child->notifyParent();
// 不需要 parent 引用,避免循环引用实战:内存泄漏检测
php
<?php
declare(strict_types=1);
function detectMemoryLeak(): void
{
$memStart = memory_get_usage();
$statusStart = gc_status();
// 执行可能产生循环引用的操作
for ($i = 0; $i < 10000; $i++) {
$a = new stdClass();
$b = new stdClass();
$a->ref = $b;
$b->ref = $a;
}
$memAfterCreate = memory_get_usage();
gc_collect_cycles();
$memAfterGC = memory_get_usage();
$statusEnd = gc_status();
echo "创建前内存: {$memStart}\n";
echo "创建后内存: {$memAfterCreate}\n";
echo "GC 后内存: {$memAfterGC}\n";
echo "GC 回收量: " . ($statusEnd['collected'] - $statusStart['collected']) . "\n";
}注意事项
WeakMap 的键必须是对象
WeakMap 的键只能是对象,不能是标量值。
WeakReference 的 get() 可能返回 null
当被引用的对象被回收后,WeakReference::get() 返回 null,调用方需要处理这种情况。
最佳实践
- 默认不担心 GC:短生命周期脚本中 GC 不是问题。
- 长运行脚本使用 WeakMap:避免循环引用导致内存泄漏。
- 定期监控内存:使用
memory_get_usage()和gc_status()监控。 - 避免不必要的双向引用:使用 ID 或事件机制替代。
php
<?php
declare(strict_types=1);
// 推荐:使用 WeakMap 存储元数据
class ObjectMetadata
{
private \WeakMap $metadata;
public function __construct()
{
$this->metadata = new \WeakMap();
}
public function set(object $obj, array $data): void
{
$this->metadata[$obj] = $data;
}
public function get(object $obj): array
{
return $this->metadata[$obj] ?? [];
}
}深入分析:GC 对长运行脚本的影响
内存使用对比
php
<?php
declare(strict_types=1);
function measureMemoryWithCycles(): void
{
$before = memory_get_usage();
for ($i = 0; $i < 5000; $i++) {
$a = new stdClass();
$b = new stdClass();
$a->ref = $b;
$b->ref = $a;
unset($a, $b);
}
$afterCreate = memory_get_usage();
echo "创建循环引用后内存增长: " . ($afterCreate - $before) . " bytes\n";
gc_collect_cycles();
$afterGc = memory_get_usage();
echo "GC 后内存减少: " . ($afterCreate - $afterGc) . " bytes\n";
}
measureMemoryWithCycles();WeakMap 替代方案详解
php
<?php
declare(strict_types=1);
class EntityMetadataManager
{
private \WeakMap $metadata;
public function __construct()
{
$this->metadata = new \WeakMap();
}
public function setTag(object $entity, string $tag, mixed $value): void
{
if (!isset($this->metadata[$entity])) {
$this->metadata[$entity] = [];
}
$this->metadata[$entity][$tag] = $value;
}
public function getTag(object $entity, string $tag, mixed $default = null): mixed
{
return $this->metadata[$entity][$tag] ?? $default;
}
public function hasTag(object $entity, string $tag): bool
{
return isset($this->metadata[$entity][$tag]);
}
public function removeEntity(object $entity): void
{
// WeakMap 的条目在对象被回收时自动删除
// 这里什么都不需要做
}
public function count(): int
{
return count($this->metadata);
}
}
$manager = new EntityMetadataManager();
$obj1 = new stdClass();
$obj2 = new stdClass();
$manager->setTag($obj1, 'dirty', true);
$manager->setTag($obj1, 'version', 3);
$manager->setTag($obj2, 'dirty', false);
echo $manager->count() . "\n"; // 2
unset($obj1);
echo $manager->count() . "\n"; // 1 — obj1 被回收避免 DOM 树的循环引用
php
<?php
declare(strict_types=1);
// 不推荐:双向引用导致循环引用
class DomNodeBad
{
public ?self $parent = null;
/** @var array<self> */
public array $children = [];
}
// 推荐:使用 ID 引用替代
class DomNodeGood
{
public ?int $parentId = null;
/** @var array<int> */
public array $childIds = [];
private static array $registry = [];
public static function register(self $node): int
{
self::$registry[] = $node;
return array_key_last(self::$registry);
}
public static function get(int $id): ?self
{
return self::$registry[$id] ?? null;
}
}