PHP 使用静态方法创建实例时的垃圾回收

PHP garbage collection when using static method to create instance

经过多次追踪,我终于弄清楚我的代码出了什么问题,所以这个问题不是“我该如何修复它”,而是“为什么会这样?".

考虑以下代码

class Foo {
    private $id;
    public $handle;

    public function __construct($id) {
        $this->id = $id;
        $this->handle = fopen('php://memory', 'r+');

        echo $this->id . ' - construct' . PHP_EOL;
    }

    public function __destruct() {
        echo $this->id . ' - destruct' . PHP_EOL;

        fclose($this->handle);
    }

    public function bar() {
        echo $this->id . ' - bar - ' . get_resource_type($this->handle) . PHP_EOL;

        return $this;
    }

    public static function create($id) {
        return new Foo($id);
    }
}

看起来很简单 - 创建时它将打开内存流并设置 属性 $handle$id。销毁时它将使用 fclose 关闭此流。

用法:

$foo = Foo::create(1); // works

var_dump( $foo->bar()->handle ); // works

var_dump( Foo::create(2)->bar()->handle ); // doesn't work

这里的问题似乎是我希望对 return 的两次调用完全相同,但由于某种原因 Foo::create(2) 调用我 将实例保存到变量调用垃圾收集器介于 bar() 方法的 return $this 部分和我实际使用 属性 $handle.

如果您想知道,这个就是输出:

1 - construct                 // echo $this->id . ' - construct' . PHP_EOL;
1 - bar - stream              // echo $this->id . ' - bar - ' ...
resource(5) of type (stream)  // var_dump
2 - construct                 // echo $this->id . ' - construct' . PHP_EOL;
2 - bar - stream              // echo $this->id . ' - bar - ' ...
2 - destruct                  // echo $this->id . ' - destruct' . PHP_EOL;
resource(6) of type (Unknown) // var_dump
1 - destruct                  // echo $this->id . ' - destruct' . PHP_EOL;

据我所知,这就是发生的事情:

var_dump( Foo::create(2)->bar()->handle );
// run GC before continuing..  ^^ .. but I'm not done with it :(

但是为什么?为什么 PHP 认为我已经完成了 variable/class 实例,因此觉得需要销毁它?

演示

eval.in demo
3v4l demo (only HHVM can figure it out - all other PHP versions can't)

It seems that it's all about variable scoping.

In short if you assign Foo::create() to a global variable you can access the handle in the global scope and the destructor won't be called until the end of the script.

Whereas if you don't actually assign it to a global variable the last method call in the local scope will trigger the destructor; the handle is closed at Foo::create(1)->bar() so ->method is now closed when you're attempting to access it.

进一步的调查表明前提是有缺陷的——这里肯定有问题!它 似乎影响资源。


案例 1

$foo = Foo::create(1);
var_dump( $foo->bar()->handle );

结果:

resource(3) of type (stream)

在这种情况下,我们已将全局变量 $foo 指定为使用 Foo::create(1) 创建的 Foo 的新实例。我们现在正在使用 bar() 访问该全局变量到 return 本身,然后是 public handle.


案例 2

$bar = Foo::create(2)->bar();
var_dump( $bar->handle );

结果:

resource(4) of type (stream)

同样,它仍然没问题,因为 Foo::create(2) 已经创建了一个 Foo 的新实例并且 bar() 只是 return 编辑了它(它仍然可以访问它本地范围)。这已分配给全局变量 $bar,并且从中检索 handle


案例3

var_dump( Foo::create(3)->bar()->handle );

结果:

resource(5) of type (Unknown)

这是因为当 Foo::create() return 是 Foo 的一个新实例时,它被 bar() 使用...但是当 bar() 关闭时没有不再 local 使用该实例并调用 __destruct() 方法关闭句柄。如果你简单地写:

,你会得到同样的结果
$h = fopen('php://memory', 'r+');
fclose($h);
var_dump($h);

如果你尝试,你会得到完全相同的结果:

var_dump( Foo::create(3)->handle );

Foo::create(3) 将调用析构函数,因为不再有对该实例的本地调用。


编辑

进一步的修修补补使水更加浑浊...

我添加了这个方法:

public function handle() {
    return $this->handle;
}

现在如果我的前提是正确的,那么做:

var_dump( Foo::create(3)->handle() );

应该导致:

resource(3) of type (stream)

...但它没有,您再次获得 Unknown 的资源类型 - 似乎在 return $this 处调用了析构函数在 public class 成员被访问之前!然而,在其上调用方法绝对没问题:

public function handle() {
    return $this->bar();
}

那会很乐意把你的东西还给你:

object(Foo)#1 (2) {
  ["id":"Foo":private]=>
  int(3)
  ["handle"]=>
  resource(3) of type (stream)
}

似乎没有办法在调用析构函数之前以这种方式访问​​资源 class 成员?!


正如 Alex Howansky 指出的那样,标量很好:

public function __destruct() {
    $this->id = 2000;
    fclose($this->handle);
}

public function handle() {
    return $this->id;
}

现在:

var_dump( Foo::create(3)->handle() );

结果:

int(3)

...原始 $id 在调用析构函数之前被 returned。

这对我来说绝对是个错误。

这一切归结为 refcounts and how PHP treats resources differently

当 class 实例被销毁时,所有非数据库 link 资源都将关闭(参见上文 link 资源)。其他地方引用的所有非资源仍然有效。

在您的第一个示例中,您分配了 $temp = Foo::create(1),这增加了对 Foo 实例的引用计数,防止它被销毁,从而保持资源打开。

在您的第二个示例中,var_dump( Foo::create(2)->bar()->handle );,结果如下:

    调用
  1. Foo::create(2),创建 Foo.
  2. 的实例
  3. 您在新实例上调用方法 bar(),返回 $this,这会将引用计数增加一。
  4. 您离开 bar() 的作用域,下一个动作不是方法调用或赋值,refcount 减一。
  5. 实例的引用计数为零,因此已销毁。所有非数据库 link 资源都已关闭。
  6. 您尝试访问已关闭的资源,返回 Unknown

作为额外的证据,这很好用:

$temp = Foo::create(3)->bar();
// $temp keep's Foo::create(3)'s refcount above zero
var_dump( $temp->handle );

这样做:

$temp = Foo::create(4)->bar()->bar()->bar();
// Same as previous example
var_dump( $temp->handle );

还有这个:

// Assuming you made "id" public.
// Foo is destroyed, but "id" isn't a resource.  It will be garbage collected later.
var_dump( Foo::create(5)->id );

这个工作:

$temp = Foo::create(6)->handle;
// Nothing has a reference to Foo, it gets destroyed, all resources closed.
var_dump($temp);

这也不行:

$temp = Foo::create(7);
$handle = $temp->handle;
unset($temp);
// $handle is now a reference to a closed resource because Foo was destroyed
var_dump($handle);

Foo被销毁时,所有打开的资源(除了数据库links)都被关闭。引用 Foo 中的其他属性仍然有效。

演示: https://eval.in/271514