【问题标题】:PHP garbage collection when using static method to create instance使用静态方法创建实例时的 PHP 垃圾收集
【发布时间】:2015-03-27 13:17:28
【问题描述】:

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

考虑以下代码

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) 调用我将实例保存到变量在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 认为我已经完成了变量/类实例,因此觉得需要销毁它?

演示

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

【问题讨论】:

  • 也许在 bar 调用之后,GC 杀死了对象,因为它没有分配给变量?我的意思是,它可能在 bar 调用之后进行操作,但是由于它没有分配给任何东西,所以它不在乎并且无论如何都会将其杀死。
  • @JamesHunt 但它仍然在类($this->handle)中定义,并且它正在返回类实例($instance->handle 被定义)?
  • 句柄可以在类中定义,但是对象本身还是需要赋值给var_dump的变量。 var_dump 使用您的参数调用,您的参数使用静态函数 Foo::create(2) 创建类 Foo 的实例,然后您可以在其中使用它的 bar 函数。 bar 函数完成后,对象本身并没有存储在变量中,它只是作为可能的参数浮动,所以可能 GC 在 var_dump 获取句柄变量之前将其清除?
  • 经过一番玩弄之后,我得出的结论是,这有点奇怪...如果在您的创建方法中将 new Foo() 分配给一个变量,然后返回该变量它可以工作没关系。 create($id) { $foo = new Foo($id); return $foo; } 然后没关系 - 即使 GC 将在方法结束时销毁 $foo(返回 $foo 时)。这可能与构造函数可能返回 null 的事实有关——尽管我在这里有点抓不住...
  • 如果你在它周围加上一些括号,它似乎也可以工作:create($id) { return (new Foo($id)); } - 现在我开始认为这与添加的类成员访问实例化有关在 PHP 5.4 中:docs.php.net/manual/en/migration54.new-features.php

标签: php garbage-collection


【解决方案1】:

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

当一个类实例被销毁时,所有非数据库链接资源都被关闭(参见上面的资源链接)。其他地方引用的所有非资源仍然有效。

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

在你的第二个例子中,var_dump( Foo::create(2)->bar()->handle );,事情是这样发展的:

  1. 调用Foo::create(2),创建Foo 的实例。
  2. 您在新实例上调用方法bar(),返回$this,这会将引用计数增加一。
  3. 您离开 bar() 的作用域,下一个操作不是方法调用或赋值,引用计数减一。
  4. 实例的引用计数为零,所以它被销毁了。所有非数据库链接资源均已关闭。
  5. 您尝试访问已关闭的资源,返回 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 被销毁时,所有打开的资源(数据库链接除外)都将关闭。来自Foo 的引用其他属性仍然有效。

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

【讨论】:

  • 在这种情况下,它是一个相当简单的可链接 OOP 结构。考虑SqlCommand::create()->select('foo')->from('bar')->where('foo.id = 3')->orderBy('foo.id ASC')->fetchRow()。即使我在技术上没有首先创建 SqlCommand 的实例,这仍然有效。这也适用于标量值,只是出于某种原因不适用于资源,那么它适用于标量值的原因是什么?他们肯定不会仅仅因为我的最后一个属性是标量值而不是资源而增加/减少引用计数吗?
  • 资源是special exception。数据库句柄是特殊异常的一个例外。我会编辑我的帖子。
【解决方案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::create(1) 创建的 Foo 的新实例。我们现在使用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() 关闭时,不再有任何本地 使用该实例并调用 __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 before 公共类成员调用了析构函数被访问了!然而,在其上调用方法绝对没问题:

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。

这对我来说绝对是个臭虫。

【讨论】:

  • 不过还是有一些古怪的事情发生。如果在构造函数中将句柄设置为标量,然后在析构函数中将其设置为 null,则在引用它时将返回预期的预置空值。仅当句柄是资源时才会发生这种取消设置的行为。我认为这是一个错误。如果$foo = new Foo(); echo $foo->bar; 的行为与echo (new Foo())->bar; 不同,那么后一种表示法就毫无用处了。
  • 哦 - 它变得更糟 - 只是做一些进一步的修补,现在我更加困惑;我会更新这个...
  • 嘿,确实是这样。我也没有在错误跟踪器中找到任何东西。
  • 非常棒的想法和测试 - 非常感谢您对它的修补。我认为这也可能是一个错误,但我肯定不是唯一一个编写了这个相当简单的代码的人,而且考虑到这个错误在 PHP 中已经存在了近 10 年,似乎没有其他人会遇到同样的问题。不过,也许他们已经并且只是懒得搞清楚为什么。我也根本不是 OPCODE 人,但唯一的区别是 ASSIGN !0, $1Works - doesn't work
  • 另请注意,虽然 HHVM 似乎可以工作,因为它输出“流”而不是“未知”——您实际上不能使用流。它将失败并显示有关无效流资源的警告。所以至少是一致的。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-20
  • 2014-03-09
  • 2016-02-29
  • 1970-01-01
相关资源
最近更新 更多