返回

PHP弱引用与即时垃圾回收错误:原理、解决方案及最佳实践

php

PHP中弱引用与即时垃圾回收导致的错误

在PHP中,利用弱引用来管理对象关系,特别是当存在循环引用时,是一种常见的实践,旨在避免内存泄漏。然而,当结合“即时”垃圾回收行为,尤其是在动态创建实例的情况下,可能会引发一些意想不到的问题。这通常表现为在使用弱引用访问对象时出现 Return value must be of type object, null returned 错误。 这种现象并非PHP本身错误,而在于理解其垃圾回收机制与弱引用相互作用。

问题剖析

当动态创建一个对象,且该对象持有一个指向其他对象的弱引用,同时,如果对该动态创建对象不再持有强引用,则其会被立即放入垃圾回收的待处理队列,当垃圾回收执行,弱引用将指向一个无效的位置。 这样的情况在使用工厂模式或者函数链式调用时比较容易出现,此时如果再使用这个弱引用就自然会引发错误。 例如以下例子:

class A {
     public readonly B $b;
     function __construct(public string $name) {
          $this->b = new B($this);
     }
    function getB(): B {
         return $this->b;
     }
}

class B {
     use weakObjReference;
    function __construct(A $container) {
         $this->weakReference($container, 'container');
    }
    function greet($msg): string{
       return  $msg.", ".$this->container->name;
    }
}

$a = new A('world 1');
var_dump($a->getB()->greet("hello"));  // 这行没问题
var_dump((new A('world 2'))->getB()->greet('good afternoon')); // 这行出现错误

错误产生的原因在于,第一行代码 $a = new A('world 1'); 生成的实例 $a 以及 其内部实例 $b 在使用完之后没有被立即清除掉。当第二行代码 (new A('world 2'))->getB()->greet('good afternoon') 执行的时候,动态创建的实例 $A 的实例,仅仅存在 __construct() 创建时被弱引用的 B 的实例所持有,随后因为动态实例 $A 没有其他引用,立即被 PHP 的垃圾回收机制处理,对应的弱引用就会失效。
这解释了为何第一条语句运行良好,而第二条却导致错误:第一个 A 实例被 $a 变量所引用,因此即使它内部的B实例使用弱引用指向它,该引用始终有效,第二个则不然,被PHP及时回收,弱引用自然失效。

解决方案

核心问题是 PHP 的“即时”垃圾回收行为过于积极,特别是在使用临时创建的对象实例时,影响了弱引用的预期表现。解决办法需从理解垃圾回收的原理入手。

1. 使用强引用临时保留对象

最直接的方法就是在你不再使用对象之前使用变量强引用它们。这样会避免该对象立即被垃圾回收。 缺点是需要在多层嵌套的情况下引入更多中间变量。例如以下示例:

class A {
     public readonly B $b;
     function __construct(public string $name) {
          $this->b = new B($this);
     }
    function getB(): B {
         return $this->b;
     }
}

class B {
     use weakObjReference;
    function __construct(A $container) {
         $this->weakReference($container, 'container');
    }
    function greet($msg): string{
       return  $msg.", ".$this->container->name;
    }
}
$temp = new A('world 2');
$temp_b = $temp->getB();

var_dump($temp_b->greet('good afternoon')); // 可以正确输出

  • 原理 : 通过 $temp = new A(...) 创建 A 的强引用,此时,弱引用能够正常获取该对象,避免了其被立刻回收。
  • 操作步骤 : 创建临时变量接收 new A(...) 创建的对象实例,并对其子实例进行操作。
  • 优点 : 方法简单易懂。
  • 缺点 : 当层级复杂时需要引入的临时变量较多, 代码变得臃肿不易读,破坏了方法调用的链式表达,代码可读性不高,在某些情景中并不实用,维护性也随之下降。
  • 额外建议 : 此方法应在迫不得已时使用,尽可能通过其他方案来解决根本问题。

2. 利用作用域,延迟垃圾回收

另一种更优雅的方法是利用作用域,将动态创建的对象的生命周期延长到当前函数或者方法的结束,以延迟其被回收。例如以下示例:


class A {
     public readonly B $b;
     function __construct(public string $name) {
          $this->b = new B($this);
     }
    function getB(): B {
         return $this->b;
     }
    static function get_b_greet(string $name,string $message): string {
          $a_instance = new A($name);
          return $a_instance->getB()->greet($message);
      }
}

class B {
     use weakObjReference;
    function __construct(A $container) {
         $this->weakReference($container, 'container');
    }
    function greet($msg): string{
       return  $msg.", ".$this->container->name;
    }
}
var_dump(A::get_b_greet("world 3","good evening"));

  • 原理 : 当把代码移到一个方法或者函数里,在执行结束后该对象所创建的作用域释放,此时php垃圾回收才会开始清除未被引用的对象,这时对象被弱引用所引用的时候依然存在于内存中。
  • 操作步骤 : 将产生动态实例代码移动到新的方法或函数里,并在方法或者函数里面完成链式方法调用。
  • 优点 : 比引入变量更好一些,提高了可读性。
  • 缺点 : 代码可维护性也并不高,代码稍微复杂就需要将原本链式调用切割开。并且如果不是即时的动态实例化,而是一个提前实例化后的静态对象方法调用,该方法并不适用。

3. 重新考虑对象之间的关系与生命周期

根本上说,或许该反思对象间的关系。是否弱引用并非在所有情况下都适用? 是否存在替代方案来表达对象之间的关系,而不需要承担弱引用的局限性? 例如,与其建立循环引用,为何不考虑通过其他途径实现依赖关系?可以重新审视系统整体的架构,简化对象结构,以减少或者消除对弱引用的需求。
例如我们可以这样修改:


class A {
     public string $name;
     public function __construct(string $name)
     {
         $this->name = $name;
     }
}


class B {

    private A|null $container;
     function __construct(?A $container=null) {
          $this->container = $container;
    }
    function greet($msg): string{
       if(!$this->container){
          return  $msg.".". "no name";
       }
        return  $msg.", ".$this->container->name;
    }
    function setContainer(?A $container):void {
       $this->container = $container;
    }
}

$a_instance = new A("world 4");
$b_instance = new B();
$b_instance->setContainer($a_instance);
var_dump($b_instance->greet("good night"));
$c_instance = new B(new A("world 5"));
var_dump($c_instance->greet("good dawn"));

  • 原理 : 把弱引用修改为通过参数形式传递 A 类的实例,或者是通过 setContainer 函数动态绑定。 B 并不依赖与构造时绑定到指定的 A 的实例。并且内部保留了 null 安全操作。
  • 操作步骤 : 重新构建系统架构。修改所有涉及到类似问题的类的对象关系,彻底清除对弱引用的依赖。
  • 优点 : 从根本上解决问题。代码结构更加清晰。可读性和可维护性更好。
  • 缺点 : 修改范围较广。 整体调整较大,需要花费较大的精力来改造代码。

总结

当即时垃圾回收与弱引用共同工作时,动态创建的PHP对象会遇到生命周期管理的问题。 本质上说,选择何种方法取决于特定场景的需求和对代码复杂性的考量。没有唯一的银弹,只有在充分了解PHP的运作机制后,才可以选取最合适方案。 如果可以修改架构来去除循环依赖关系或者解除使用弱引用的场景,那无疑是更好的方案。