オブジェクトが例外の呼び出し履歴に参照されてデストラクタが呼ばれない

PHP でリソース開放などの後処理をデストラクタに行わせるのは無理がありそうなので finally が早く使えるようになるといいなーとは常々思っていましたが、また新たに次のような期待に反してデストラクタが呼ばれないケースが見つかりました。

サンプルコード
<?php
class Hoge
{
	private $_str;
	
	public function __construct($str)
	{
		$this->_str = $str;
		echo __METHOD__ . " $this->_str\n";
	}
	
	public function __destruct()
	{
		echo __METHOD__ . " $this->_str\n";
	}
}

function main()
{
	echo __FUNCTION__ . " begin\n";
	main2();
	echo __FUNCTION__ . " end\n";
}

function main2()
{
	echo __FUNCTION__ . " begin\n";
	
	try
	{
		main3();
	}
	catch (Exception $ex)
	{
		// (1)
		// echo "catch begin\n";
		// unset($ex);
		// echo "catch end\n";
	}
	
	echo __FUNCTION__ . " end\n";
}

function main3()
{
	echo __FUNCTION__ . " begin\n";
	$a = new Hoge(__FUNCTION__);
	main4($a);
	echo __FUNCTION__ . " end\n";
}

function main4($a)
{
	// (2)
	// throw new Exception("except");
}

main();
実行結果

そのまま実行すると次のような出力になります。

main begin
main2 begin
main3 begin
Hoge::__construct main3
main3 end
Hoge::__destruct main3
main2 end
main end


main3 の $a のデストラクタは main3 を抜けたときに呼ばれており、期待した通りの動作です。

例外を投げてみる

(2) をコメントインして main4 で例外を投げるようにすると次のようになります。

main begin
main2 begin
main3 begin
Hoge::__construct main3
main2 end
Hoge::__destruct main3
main end


例外を throw して catch するまでの間に main3 を突き抜けているので、そのタイミングでデストラクタが呼ばれると思ったのですがそんなことはありませんでした。この例の場合、デストラクタは main2 を抜けた時に呼ばれています。

例外をアンセットしてみる

さらに (1) の3行をコメントインすると次のようになります。

main begin
main2 begin
main3 begin
Hoge::__construct main3
catch begin
Hoge::__destruct main3
catch end
main2 end
main end


$a のデストラクタは例外を unset したときに呼ばれていることがわかります。

まとめ


何故このような動作になるのかは、例外の getTrace を見れば一目瞭然でした。(1) の unset($ex) の直前に var_dump を仕込んでみます。

<?php
		// (1)
		var_dump(array_slice($ex->getTrace(), 0, 1));
		echo "catch begin\n";
		unset($ex);
		echo "catch end\n";


var_dump の出力は次のようになります。

array(1) {
  [0]=>
  array(4) {
    ["file"]=>
    string(19) "/path/to/sample.php"
    ["line"]=>
    int(57)
    ["function"]=>
    string(5) "main4"
    ["args"]=>
    array(1) {
      [0]=>
      object(Hoge)#1 (1) {
        ["_str":"Hoge":private]=>
        string(5) "main3"
      }
    }
  }
}


例外のスタックトレースに関数の引数がそのまま保存されています。そのため、例外から間接的にオブジェクトが参照されてしまい、例外が消えるまでオブジェクトが破棄されないままになってしまうのです。