PHP使用弱引用时即时垃圾回收引发错误的优雅解决方案
解决弱引用在临时对象链式调用中的GC提前回收问题
问题重现
我维护的大型对象结构存在大量一对多反向引用,为避免循环引用无法被垃圾回收,使用了trait weakObjReference通过弱引用存储容器对象,多年运行稳定。但在链式调用未赋值给变量的临时对象时,出现弱引用提前失效的问题:
// 简化的问题代码 class a { public readonly b $b; function __construct(public string $name) { $this->b = new b($this); } function hoohoo(): mixed { return $this->b; } } class b { use weakObjReference; function __construct(a $container) { $this->weakReference($container, 'container'); } function heehee($name): mixed { return $name . ', '. $this->container->name; } } // 正常运行 $a = new a('world 1'); var_dump($a->hoohoo()->heehee('hello')); // 输出 'hello, world 1' // 触发GC错误 var_dump((new a('world 2'))->hoohoo()->heehee('good afternoon')); // 错误:weakObjReferenceStorage::get(): Return value must be of type object, null returned
问题根源
临时创建的a实例没有被强引用变量持有,PHP的垃圾回收机制会在表达式执行过程中(而非语句结束后)触发回收:当调用(new a())->hoohoo()返回b实例后,a的引用计数归零,直接被GC回收,导致b中的弱引用失效。
之前通过手动临时转为强引用的方案需要在多类中编写大量维护代码,成本过高。
优雅解决方案:自动代理强引用
修改trait weakObjReference,在访问弱引用属性时返回代理对象,由代理自动持有原对象的强引用,直到代理被销毁(链式调用结束),完全无需用户层额外代码:
trait weakObjReference { /** @var array<string, WeakReference> 存储弱引用映射 */ private array $_weakRefs = []; /** * 绑定弱引用属性 * @param object $obj 要绑定的容器对象 * @param string $propName 要映射的属性名 */ public function weakReference(object $obj, string $propName): void { $this->_weakRefs[$propName] = WeakReference::create($obj); } /** * 重载属性访问,返回代理对象持有强引用 * @param string $propName 属性名 * @return object|null * @throws RuntimeException 弱引用已被回收时抛出 */ public function __get(string $propName): mixed { if (!isset($this->_weakRefs[$propName])) { trigger_error(sprintf('Undefined property: %s::$%s', static::class, $propName), E_USER_NOTICE); return null; } $targetObj = $this->_weakRefs[$propName]->get(); if ($targetObj === null) { throw new RuntimeException(sprintf('Weak reference for property "%s" has been garbage collected', $propName)); } // 创建匿名代理类,持有目标对象的强引用 return new class($targetObj) { private object $_target; public function __construct(object $target) { $this->_target = $target; } // 代理属性访问 public function __get(string $prop): mixed { return $this->_target->$prop; } // 代理方法调用 public function __call(string $method, array $args): mixed { return $this->_target->$method(...$args); } // 代理isset检查 public function __isset(string $prop): bool { return isset($this->_target->$prop); } }; } }
原理说明
- 当访问
$this->container时,trait不再直接返回弱引用的对象,而是返回一个匿名代理实例; - 代理实例内部持有目标对象(比如
a)的强引用,确保在链式调用过程中目标对象不会被GC回收; - 链式调用结束后,代理实例被销毁,强引用自动释放,目标对象可以正常被GC回收;
- 代理自动处理所有属性访问和方法调用,对用户层完全透明,无需修改原有业务代码。
验证效果
使用修改后的trait,示例2的链式调用会正常执行,输出string(27) "good afternoon, world 2",同时不会引入新的循环引用,GC可以正常回收临时对象。
内容的提问来源于stack exchange,提问作者Roemer
相关产品推荐
相关产品推荐

