28

オブジェクトの分解の正確な順序は何ですか?

テストから、私は考えを持っています:現在のスコープのFIFO。

class test1
{
    public function __destruct()
    {
        echo "test1\n";
    }
}

class test2
{
    public function __destruct()
    {
        echo "test2\n";
    }
}

$a = new test1();
$b = new test2();

これは何度も同じ結果を生成します:

test1
test2

PHPのマニュアルはあいまいです(不確実性を強調するために強調しています):「シャットダウンシーケンス中に特定のオブジェクトへの参照が他になくなるとすぐに、または任意の順序でデストラクタメソッドが呼び出されます。」

脱構築の正確な順序は何ですか?PHPが使用する破棄順序の実装について詳しく説明できますか?また、この順序がすべてのPHPバージョン間で一貫していない場合、どのPHPバージョンがこの順序で変更されるかを誰かが特定できますか?

4

3 に答える 3

39

まず、一般的なオブジェクトの破棄順序について少し説明します:https ://stackoverflow.com/a/8565887/385378

この回答では、リクエストのシャットダウン中にオブジェクトがまだ生きている場合、つまり、オブジェクトが以前にrefcountingメカニズムまたは循環ガベージコレクターによって破棄されていなかった場合に何が起こるかについてのみ関心があります。

PHPリクエストのシャットダウンはphp_request_shutdown関数で処理されます。シャットダウン中の最初のステップは、登録されたシャットダウン関数を呼び出し、その後それらを解放することです。これにより、シャットダウン関数の1つがオブジェクトへの最後の参照を保持していた場合(またはシャットダウン関数自体がオブジェクト、たとえばクロージャーであった場合)、オブジェクトが破壊される可能性もあります。

シャットダウン関数が実行された後、次のステップはあなたにとって興味深いものです。PHPが実行さzend_call_destructorsれ、次にが呼び出されますshutdown_destructors。この関数は、次の3つのステップですべてのデストラクタを呼び出します(しようとします)。

  1. 最初に、PHPはグローバルシンボルテーブル内のオブジェクトを破棄しようとします。これが発生する方法はかなり興味深いので、以下のコードを再現しました。

    int symbols;
    do {
        symbols = zend_hash_num_elements(&EG(symbol_table));
        zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor TSRMLS_CC);
    } while (symbols != zend_hash_num_elements(&EG(symbol_table)));
    

    このzend_hash_reverse_apply関数は、シンボルテーブルを逆方向​​にウォークします。つまり、最後に作成された変数から開始し、最初に作成された変数に向かって進みます。歩いている間、refcount 1ですべてのオブジェクトが破棄されます。この反復は、それ以上オブジェクトが破棄されなくなるまで実行されます。

    したがって、これは基本的に、a)グローバルシンボルテーブル内のすべての未使用オブジェクトを削除しますb)新しい未使用オブジェクトがある場合は、それらも削除しますc)など。この破壊方法は、オブジェクトがデストラクタ内の他のオブジェクトに依存できるようにするために使用されます。グローバルスコープ内のオブジェクトに複雑な(循環などの)相互関係がない限り、これは通常は正常に機能します。

    グローバルシンボルテーブルの破棄は、他のすべてのシンボルテーブルの破棄とは大きく異なります。通常、シンボルテーブルは、前方に移動し、すべてのオブジェクトにrefcountをドロップするだけで破棄されます。一方、グローバルシンボルテーブルの場合、PHPはオブジェクトの依存関係を尊重しようとするよりスマートなアルゴリズムを使用します。

  2. 2番目のステップは、残りのすべてのデストラクタを呼び出すことです。

    zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);
    

    これにより、すべてのオブジェクトが(作成順に)ウォークされ、それらのデストラクタが呼び出されます。これは「dtor」ハンドラーのみを呼び出し、「free」ハンドラーは呼び出さないことに注意してください。この区別は内部的に重要であり、基本的にPHPはを呼び出すだけ__destructで、実際にはオブジェクトを破棄しない(またはそのrefcountを変更しない)ことを意味します。したがって、他のオブジェクトがdtoredオブジェクトを参照している場合、それは引き続き使用可能です(デストラクタがすでに呼び出されている場合でも)。彼らは、ある意味で、ある種の「半分破壊された」オブジェクトを使用します(以下の例を参照)。

  3. デストラクタの呼び出し中に実行が停止した場合(たとえば、die)、残りのデストラクタは呼び出されません。代わりに、PHPはオブジェクトがすでに破壊されていることを示します。

    zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
    

    ここでの重要な教訓は、PHPではデストラクタが必ずしもと呼ばれるとは限らないということです。これが発生するケースはかなりまれですが、発生する可能性があります。さらに、これは、この時点以降、デストラクタが呼び出されなくなることを意味します。したがって、(かなり複雑な)シャットダウン手順の残りの部分は、もはや重要ではありません。シャットダウン中のある時点で、すべてのオブジェクトが解放されますが、デストラクタはすでに呼び出されているため、これはユーザーランドでは目立ちません。

これが現在のシャットダウン順序であることを指摘しておく必要があります。これは過去に変更されており、将来変更される可能性があります。それはあなたが頼るべきものではありません。

すでに破壊されたオブジェクトを使用する例

これは、デストラクタがすでに呼び出されているオブジェクトを使用できる場合があることを示す例です。

<?php

class A {
    public $state = 'not destructed';
    
    public function __destruct() { $this->state = 'destructed'; }
}

class B {
    protected $a;
    
    public function __construct(A $a) { $this->a = $a; }
    
    public function __destruct() { var_dump($this->a->state); }
}
    
$a = new A;
$b = new B($a);

// prevent early destruction by binding to an error handler (one of the last things that is freed)
set_error_handler(function() use($b) {});

上記のスクリプトはを出力しますdestructed

于 2012-12-31T12:43:26.073 に答える
9

脱構築の正確な順序は何ですか?PHPが使用する破棄順序の実装について詳しく説明できますか?また、この順序がすべてのPHPバージョン間で一貫していない場合、この順序が変更されるPHPバージョンを誰かが特定できますか?

やや回りくどい方法で、これらのうち3つに答えることができます。

破棄の正確な順序は必ずしも明確ではありませんが、単一のスクリプトとPHPバージョンがあれば常に一貫しています。つまり、同じパラメータで実行され、同じ順序でオブジェクトを作成する同じスクリプトは、同じPHPバージョンで実行されている限り、基本的に常に同じ破棄順序になります。

シャットダウンプロセス(スクリプトの実行が停止したときにオブジェクトの破棄をトリガーするプロセス)、最近、破棄順序に間接的に影響を与える方法で少なくとも2回変更されました。これら2つのバグの1つは、私が維持しなければならなかった古いコードにバグをもたらしました。

大きなものは5.1に戻った。5.1より前では、ユーザーのセッションは、オブジェクトが破棄される前のシャットダウンシーケンスの開始時にディスクに書き込まれていました。これは、セッションハンドラーが、たとえばカスタムデータベースアクセスオブジェクトなど、オブジェクトごとに残されたものすべてにアクセスできることを意味しました。5.1では、セッションはオブジェクト破壊の1回のスイープの後に書き込まれました。以前の動作を維持するには、書き込みルーチンに(グローバル)オブジェクトが必要な場合にセッションデータを正常に書き込むために、シャットダウン関数(破棄 前のシャットダウンの開始時に定義順に実行される)を手動で登録する必要がありました。

5.1の変更が意図されたものなのか、それともバグだったのかは明らかではありません。私は両方が主張しているのを見てきました。

次の変更は5.3で、新しいガベージコレクションシステムが導入されました。シャットダウン時の操作の順序は同じままでしたが、参照カウントやその他の楽しい恐怖に基づいて、破壊 の正確な順序が変わる可能性があります。

NikiCの回答には、シャットダウンプロセスの現在の(執筆時点での)内部実装に関する詳細が含まれています。

繰り返しになりますが、これはどこでも保証されているわけではなく、ドキュメントには、破棄命令を決して想定しないように非常に明確に記載されています

于 2012-12-31T08:01:13.393 に答える
0

興味のある人のために-PHP8.0のように:

class A {
  
  function __destruct() {
    print get_class();
  }
}

class B {
  private $child;

  function __construct() {
    $this->child = new A();
  }
  
  function __destruct() {
    print get_class();
  }
}

class C {
  private $child;

  function __construct() {
    $this->child = new B();
  }
  
  function __destruct() {
    print get_class();
  }
}


new C;

結果は次のようになります

CBA

すなわち。包含オブジェクトデストラクタは、包含オブジェクトデストラクタの前に起動します。

必要に応じて順序を逆にします。ABCに、A(最も内側のクラス)を除くすべてのデストラクタを次のように変更します。

function __destruct() {
  unset($this->child);
  print get_class();
}
于 2021-12-18T13:22:06.813 に答える