Каков точный порядок деконструкции объекта?
Из тестирования у меня есть идея: 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 этот порядок изменяется?
Прежде всего, здесь описывается бит в общем порядке уничтожения объектов: https://stackoverflow.com/a/8565887/385378
В этом ответе я буду заниматься только тем, что происходит, когда объекты остаются живыми во время выключения запроса, то есть, если они ранее не были уничтожены с помощью механизма refcounting или кругового сборщика мусора.
Отключение запроса PHP обрабатывается в функции php_request_shutdown
. Первый шаг во время выключения – это вызов зарегистрированных функций выключения и последующее их освобождение. Это, очевидно, также может привести к разрушению объектов, если одна из функций выключения удерживала последнюю ссылку на какой-либо объект (или если сама функция выключения была объектом, например закрытием).
После запуска функций останова вам будет zend_call_destructors
следующий шаг: PHP запустит zend_call_destructors
, который затем вызывает shutdown_destructors
. Эта функция (попытается) вызвать всех деструкторов в три этапа:
Первый 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
будет zend_hash_reverse_apply
по таблице символов назад , то есть начинать с переменной, которая была создана последней, и перейти к первой, которая была создана. Во время ходьбы он уничтожит все объекты с помощью refcount 1. Эта итерация выполняется до тех пор, пока с ней не будут уничтожены никакие другие объекты.
Так что в основном это: a) удалить все неиспользуемые объекты в глобальной таблице символов; b) если есть новые неиспользуемые объекты, удалите их c) и так далее. Этот способ разрушения используется, поэтому объекты могут зависеть от других объектов в деструкторе. Обычно это нормально, если объекты в глобальном масштабе не имеют сложных (например, круговых) взаимосвязей.
Уничтожение глобальной таблицы символов значительно отличается от уничтожения всех других таблиц символов. Обычно таблицы символов разрушаются, перемещая их вперед и просто отбрасывая refcount для всех объектов. С другой стороны, для глобальной таблицы символов PHP использует более интеллектуальный алгоритм, который пытается уважать зависимости объектов.
Второй шаг – вызов всех остальных деструкторов:
zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);
Это будет идти по всем объектам (в порядке творения) и вызвать их деструктор. Обратите внимание, что это вызывает только обработчик «dtor», но не «свободный» обработчик. Это различие является внутренне важным и в основном означает, что PHP будет вызывать только __destruct
, но фактически не уничтожит объект (или даже изменит его refcount). Поэтому, если другие объекты ссылаются на объект dtored, он все равно будет доступен (хотя деструктор уже был вызван). Они будут использовать какой-то «полуразрушенный» объект, в некотором смысле (см. Пример ниже).
В случае остановки выполнения вызова при вызове деструкторов (например, из-за die
) остальные деструкторы не вызываются. Вместо этого PHP отметит, что объекты уже разрушены:
zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
Важным уроком здесь является то, что в PHP деструктор не обязательно называется . Случаи, когда это происходит, довольно редки, но это может произойти. Кроме того, это означает, что после этого момента больше не будут вызваны деструкторы , поэтому оставшаяся часть (довольно сложная) процедура останова не имеет значения. В какой-то момент во время выключения все объекты будут освобождены, но поскольку деструкторы уже были вызваны, это не заметно для userland.
Я должен указать, что это заказ выключения, как он есть сейчас. Это изменилось в прошлом и может измениться в будущем. Это не то, на что вы должны положиться.
Вот пример, показывающий, что иногда можно использовать объект, который уже вызвал деструктор:
<?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
.
Каков точный порядок деконструкции? Может ли кто-нибудь подробно описать реализацию порядка уничтожения, который использует PHP? И если этот порядок не согласуется между любыми версиями PHP, может ли кто-нибудь определить, какие версии PHP этот порядок изменяется?
Я могу ответить на три из них для вас, несколько обходным путем.
Точный порядок уничтожения не всегда ясен, но всегда согласуется с одним сценарием и версией PHP. То есть тот же скрипт, который работает с теми же параметрами, которые создают объекты в одном порядке, в основном всегда будет иметь тот же порядок уничтожения, если он работает на одной и той же версии PHP.
Процесс закрытия – то, что вызывает разрушение объекта при прекращении выполнения сценария, изменилось в недавнем прошлом, по крайней мере, дважды, что косвенно повлияло на порядок уничтожения. Один из этих двух введенных ошибок в каком-то старом коде, который мне пришлось поддерживать.
Большая была в 5.1. До 5.1 сеанс пользователя записывался на диск в самом начале последовательности выключения, до уничтожения объекта. Это означало, что обработчики сеансов могли получить доступ к чему-либо, что было оставлено по-объектно, например, к пользовательским объектам доступа к базе данных. В 5.1 сеансы были написаны после одной развертки уничтожения объекта. Чтобы сохранить предыдущее поведение, вам пришлось вручную зарегистрировать функцию выключения (которые выполняются в порядке определения в начале выключения перед уничтожением), чтобы успешно записывать данные сеанса, если для процедур записи нужен (глобальный) объект.
Неясно, было ли изменение 5.1 или было ошибкой. Я видел, как оба утверждали.
Следующее изменение было в 5.3, с введением новой системы сбора мусора . В то время как порядок операций при остановке остался прежним, точный порядок уничтожения теперь может измениться на основе пересчета и других восхитительных ужасов.
Ответ NikiC содержит подробную информацию о текущей (во время написания) внутренней реализации процесса выключения.
И снова это нигде не гарантируется, и в документации очень четко сказано, что вы никогда не принимаете порядок уничтожения .