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

时间:2015-01-28 15:11:05

标签: php garbage-collection

经过多次追踪后,我终于弄清楚我的代码中出了什么问题,所以这个问题不是" 如何解决?" ,而是" 为什么会发生这种情况?"。

请考虑以下代码

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

这里的问题似乎是我希望这两个来电都返回完全相同,但出于某种原因Foo::create(2)来电,我将实例保存到变量会在return $this方法的bar()部分和实际使用属性$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认为我已经完成了变量/类实例,因此感觉需要破坏它?

演示

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

2 个答案:

答案 0 :(得分:8)

这一切归结为refcounts以及PHP如何处理resources differently

销毁类实例时,将关闭所有非数据库链接资源(请参阅上面的资源链接)。其他地方引用的所有非资源仍然有效。

在第一个示例中,您指定了$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. 实例的引用计数为零,因此它被销毁。所有非数据库链接资源都将关闭。
  6. 您尝试访问已关闭的资源,返回Unknown
  7. 作为补充证据,这很好用:

    $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时,将关闭所有打开的资源(数据库链接除外)。引用Foo中的其他属性仍然有效。

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

答案 1 :(得分:2)

  

似乎所有关于变量范围的都是。

     

简而言之,如果您将Foo::create()分配给全局变量,则可以   访问全局范围内的handle,析构函数不会成功   直到脚本结束才被调用。

     

然而,如果你实际上没有将它分配给全局变量   本地范围内的方法调用将触发析构函数;手柄   已于Foo::create(1)->bar()关闭,因此->method现已关闭   你试图访问它。

进一步的调查显示,前提是有缺陷的 - 这里肯定会发生一些奇怪的事情! 似乎会影响资源。


案例1

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

结果:

resource(3) of type (stream)

在这种情况下,我们已将全局变量$foo指定为使用Foo创建的Foo::create(1)的新实例。我们现在使用bar()访问该全局变量,然后返回公共handle


案例2

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

结果:

resource(4) of type (stream)

同样,它仍然可以,因为Foo::create(2)创建了Foo的新实例,而bar()只返回了它(它仍然可以在本地范围内访问它) )。已将其分配给全局变量$bar,并且正在检索handle


案例3

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

结果:

resource(5) of type (Unknown)

这是因为当Foo::create()返回Foo的新实例时,bar()使用了该实例...但是当bar()关闭时{39}}不再使用该实例的任何本地,并且调用__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 function handle() {
    return $this->bar();
}

那将很乐意回馈你的对象:

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

在调用析构函数之前,似乎无法以这种方式访问​​资源类成员?!


正如 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。

这对我来说肯定闻起来像个臭虫。