Вопрос освобождения ресурсов

Программы обычно пишут так, что за время их выполнения происходит создание и уничтожение большого числа самых разнообразных переменных. Действительно, когда РНР выходит из некоторой функции, он освобождает память, используемую всеми локальными переменными внутри этой функции. Если переменные имеют простую структуру (числа, строки или даже массивы), то обычное уничтожение - это как раз то, что нам нужно. Однако при использовании объектов ситуация усложняется - возникает проблема корректного освобождения ресурсов. Это объясняется тем, что объект при работе может использовать не только собственные свойства, но также и другие объекты, а также, что самое важное, различные внешние ресурсы (файлы, потоки, соединения с СУБД и т. д.).

Пусть, например, некоторый объект открывает в своем конструкторе файл. Вызов методов этого объекта позволяет каким-либо образом манипулировать с содержимым данного файла, например, считывать или записывать строки. Но ведь в конце работы файл необходимо закрыть, иначе при интенсивном создании и уничтожении объектов­файлов рано или поздно лимит на число открытых файловых дескрипторов окажется превышенным.

В листинге 9 приведен пример такого класса. Он оказывается довольно удобным на практике для ведения разных журналов. Главное достоинство классов - умение добавлять в каждую выводимую строчку сведения о текущей дате, причем независимо от того, имеются ли в переменной символы переноса строки или нет.

Вопрос освобождения ресурсов - student2.ru

Вопрос освобождения ресурсов - student2.ru

Листинr 9 Явное освобождение ресурсов. Файл File/LoggerO.php

Как видите, для ускорения работы данные не выводятся в файл сразу же после поступления: вначале они накапливаются в буфере $lines и записываются в журнал только во время выполнения close (). У нас получается своеобразная буферизация вывода (наподобие той, что встроена в РНР, но только на уровне каждого журнала).

Рассмотрим теперь скрипт, который использует данный класс (листинг 10). Предположим, мы ошиблись и не вызвали метод close () перед входом в очередную итерацию цикла (в примере правильный вызов close () отключен).

Вопрос освобождения ресурсов - student2.ru

Листинг 10 Явное освобождение ресурсов. Файл destr0.php

В результате наш журнал test.log окажется пустым. Нам нужен какой-то механизм, который позволял бы гарантированно вызывать некоторый метод объекта, когда этот объект перестает использоваться в программе (и удаляется из памяти).

Описание деструктора

По аналогии с конструкторами обычно рассматриваются деструкторы. Деструктор - специальный метод объекта, который вызывается при уничтожении этого объекта (например, после завершения программы). Деструкторы обычно выполняют служебную работу - закрывают файлы, записывают протоколы работы, разрывают соединения, "форматируют жесткий диск" - в общем, освобождают ресурсы.

Деструктор - это специальный метод класса с именем _ destruct () , который будет гарантированно вызван при потере последней ссылки на объект в программе. Так как деструктор запускается самим РНР, он не должен принимать никаких параметров.

В листинге 11 приведен модифицированный класс с именем FileLogger, в котором объявляется деструктор. Теперь нам уже нет необходимости заботиться о "ручном" вызове close () в программе - РНР выполняет "финализирующие" действия самостоятельно.

Вопрос освобождения ресурсов - student2.ru

Вопрос освобождения ресурсов - student2.ru

Листинг 11 Деструктор. Файл File/Logger.php

Вопрос освобождения ресурсов - student2.ru

Листинг 12 Использование класса с деструктором. Файл destr.php

Посмотрите на листинг 12. В нем мы последовательно создаем 10 объектов класса FileLogger, полагаясь на то, что их деструкторы будут вызваны в нужное время. Зададимся важным вопросом: в какой именно момент это произойдет? Интуитивно понятно, что деструктор вызывается в тот момент, когда объект в программе больше не нужен, и память, отведенную под него, можно освободить. Это событие в нашем случае происходит при перезаписи переменной $logger, т. е. когда ей присваивается новое значение (первая команда цикла).

Но задумайтесь, как РНР определяет, когда объект больше не нужен и его можно удалять из памяти? В нашем случае все просто: в единицу времени на объект ссылается лишь одна переменная $logger, но представьте, что бы произошло, если бы это оказалось не так. Например, мы можем накапливать объекты FileLogger в каком-нибудь массиве, и тогда уже на каждом обороте цикла вызова деструктора не произойдет:

Вопрос освобождения ресурсов - student2.ru

Если вы модифицируете программу таким образом, то обнаружите, что она попрежнему работает! Записи в lоg-файл test.log добавляются, причем их очередность остается той же, что была ранее. Но в какой же момент РНР вызывает деструкторы десяти объектов в этом случае?

Алгоритм сбора мусора

Как мы знаем, в РНР существует такое понятие, как ссылка на объект. Ссылочная переменная хранит не сам объект, а лишь его адрес в памяти - таким образом, на один и тот же объект могут ссылаться сразу несколько переменных. Забегая вперед скажем, что объекты, на которые в программе не осталось ссылок, РНР немедленно удаляет из памяти (предварительно вызвав деструкторы). Вся специфика заключена в словах "не осталось ссылок" и "немедленно".

ПРИМЕЧАНИЕ

Представьте, что объект - это пальто, сданное в гардероб. Тогда в качестве ссылки будет выступать номерок на это пальто, выдаваемый гардеробщиком. Этот номерок можно "копировать" - например, отдав в мастерскую (аналог присваивания переменных). При этом пальто остается тем же самым и не изменяется. Что произойдет с пальто, если человек уничтожит свой номерок (обнулит ссылку на объект)? .. Наверное, через некоторое время гардеробщик сообразит, что пальто больше не нужно и лишь занимает вешалку, и отправит его на утилизацию - диспетчер динамической памяти (или, как его еще называют, сборщик мусора) удалит объект-пальто. Однако РНР гораздо "шустрее": если гардеробщику требуется некоторое время на принятие решения, то интерпретатор сразу же обнаруживает объекты, на которые нет ссылок, и удаляет их, не задерживаясь.

Сложности начинаются, когда на некоторый объект имеется более одной ссылки. В этом случае, конечно же, уничтожение нужно провести только при обнулении последней ссылки, но ни в коем случае - промежуточных. Но как же определить, ссылается ли кто-то еще на объект или же нет? ..

В этом и заключена специфика алгоритма со счетчиком ссылок, применяемого в РНР (а также в Perl), одновременно его сила и слабость. Любой объект, который вы создаете, содержит в себе скрытое поле, хранящее так называемый счетчик ссылок. Каждый раз, когда в программе появляется новая ссылка на объект, этот счетчик увеличивается на 1 ( обычно это происходит при выполнении операции присваивания $alias = $source: раньше ссылка хранилась только в $source, а теперь и в $alias, и в $source). Соответственно, при удалении ссылки счетчик уменьшается на 1. Например, операция unset ( $alias), $alias - "что угодно", а также выход локальной переменной функции за область видимости приводит к потере ссылки на объект, которая раньше находилась в $alias. Ясно, что при обнулении счетчика на объект больше никто не ссылается, а потому его можно спокойно удалить из памяти, что РНР и делает. Таким образом, объект удаляется после некоторой операции присваивания, приводящей к потере последней ссылки на него.

Удаление объекта или массива - довольно сложная процедура. Интерпретатору необходимо:

· удалить все ссылки, которые содержит сам этот объект (например, при удалении массива нужно обнулить все элементы, которые в нем содержатся - на случай, если они сами являются объектами). Если в процессе этой операции какой-то другой подчиненный объект теряет последнюю ссылку, то он также будет удален, и т. д. -рекурсивно;

· вызвать деструктор; деструкторы играют весьма важную роль в ООП, так что полная их поддержка в алгоритме со счетчиком ссылок - это сильная сторона метода;

· освободить занимаемую память; эта операция выполняется в самый последний момент и может рассматриваться как низкоуровневая.

Циклические ссылки

Алгоритмы сборки мусора с использованием счетчика ссылок, как правило, имеют один очень существенный недостаток. Речь идет о циклических ссылках. Давайте рассмотрим пример (листинг 13).

Вопрос освобождения ресурсов - student2.ru

Листинг 13 Проблемы алгоритма со счетчиком ссылок. Файл refcount.php

В программе создаются два объекта: $father и $child. При этом объект-отец хранит ссылки на всех своих потомков, а каждый сын - ссылку на отца. Это и называется циклическими ссылками: если идти "вдоль них", мы никогда не остановимся. Цикличе­ские ссылки встречаются на практике очень часто, особенно при описании иерархических структур.

Теперь взгляните на предпоследнюю строчку кода. Мы присваиваем ссылочным переменным $father и $child значение NULL, в результате чего счетчик ссылок в соответствующих объектах уменьшается на 1.

А теперь - "сюрприз": несмотря на то, что в программе мы уже никак не сможем "добраться" до данных объектов $father и $child (мы же уничтожили эти ссылки), память для них все же не освобождается, и они остаются "висеть" мертвым грузом, хотя к ним уже и нельзя получить доступ! Убедиться в этом можно, запустив скрипт листинга 13 в браузере:

Пока что все живы ... Убиваем всех.

все умерли, конец программы.

Father умер.

Child умер.

Как видите, сообщение "Все умерли", выводящееся в конце программы, оказывается самым первым, а не последним по списку. Это означает, что деструкторы были вызваны уже после завершения работы скрипта.

Давайте теперь в качестве эксперимента уберем строчку: $father->children[] = $child. Таким образом, теперь в программе уже не будет кольцевых ссылок, и результат ее работы станет выглядеть так:

Пока что все живы ... Убиваем всех.

Child умер.

Father умер.

Все умерли, конец программы.

Как видите, если циклических ссылок в программе нет, объекты уничтожаются в правильном порядке.

Проблема циклических ссылок

Все дело в злополучных счетчиках ссылок. Смотрите: $father ссылается на $child, а $child - на $father. Это значит, что и у того, и у другого счетчик равен 1 (ведь на каждого из них ссылается другой)! Стоит ли удивляться, что сборщик мусора не сработал? .. Ведь в программе не осталось ни одного объекта с нулевым счетчиком ссылок.

ПРИМЕЧАНИЕ

Аналогия с гардеробом: вы сдаете в него свое пальто, а также чужое (которое взяли только что, например, по поддельному номерку). При этом (для конспирации) номерок от своего пальто вы кладете в карман чужого, а от чужого - в карман своего. Проделав данную махинацию, вы обнаружите, что не можете больше получить одежду!

Мы рассмотрели пример циклических ссылок с "длиной цикла", равной двум. Однако, конечно, РНР попадает в безвыходную ситуацию и в случае большей косвенности: А ссылается на В, В ссылается на С, С ссылается на А.

ПРИМЕЧАНИЕ

Еще одна аналогия - известная безвыходная ситуация "ключи от машины в квартире, ключи от квартиры в сейфе, ключи от сейфа - в машине".

Еще более примечателен по своей простоте следующий код:

Вопрос освобождения ресурсов - student2.ru

Чем не замкнутая внутри себя вселенная? .. Мы получили объект, который, несмотря на потерю последней ссылки в программе, все равно продолжает существовать в памяти, занимая место, но не будучи доступным.

Итак, общий вывод: алгоритм сборки мусора и автоматического вызова деструкторов попросту "не срабатывает", когда в программе имеются кольцевые ссылки.

Проблема утечки памяти в результате циклических ссылок была неразрешима до версии РНР 5.3, начиная с которой в сборщик мусора РНР внедрен синхронный механизм сбора циклических ссылок.

Вкратце, все объекты, генерирующие ссылки, помещаются в специальный буфер, который называется корневым. При заполнении буфера (а его размер составляет 10000) стартует процедура сборки мусора, в результате которой происходит обход дерева всех ссылающихся элементов, алгоритм разрешает циклы и корректирует счетчики. Объекты, чьи счетчики стали равны нулю, удаляются. Механизм довольно ресурсоемок и включается только по заполнению буфера. По умолчанию сборщик мусора включен; если ваши скрипты работают короткое время и потребляют мало памяти, можно увеличить производительность за счет отключения сборщика мусора, установив значение директивы\ zend.enable_gc в конфигурационном файле php.ini в off.

Задания для самостоятельного решения

Вопрос освобождения ресурсов - student2.ru

Вопрос освобождения ресурсов - student2.ru

Вопрос освобождения ресурсов - student2.ru

Вопрос освобождения ресурсов - student2.ru

Вопрос освобождения ресурсов - student2.ru

Наши рекомендации