Переменная PHP хранится в контейнере, называемом "zval". Контейнер zval, помимо типа и значения переменной, также содержит два дополнительных элемента. Первый называется "is_ref" и представляет булево значение, указывающее, является переменная частью "набора ссылок" или нет. Благодаря этому элементу PHP знает как отличать обычные переменные от ссылок. Так как PHP содержит пользовательские ссылки, которые можно создать оператором &, контейнер zval также содержит внутренний механизм подсчёта ссылок для оптимизации использования памяти. Эта вторая часть дополнительной информации, называемая "refcount" (счётчик ссылок), содержит количество имён переменных (также называемых символами), которые указывают на данный контейнер zval. Все имена переменных хранятся в таблице имён, отдельной для каждой области видимости переменных. Такая область видимости существует для главного скрипта, а также для каждой функции и метода.
Контейнер zval создаётся при создании новой переменной, которой присваивается константа, например:
Пример #1 Создание нового контейнера zval
<?php
$a = "new string";
?>
В данном примере создаётся новый символ a
в текущей области видимости
и новый контейнер переменной с типом string и значением new string
.
Бит "is_ref" по умолчанию задаётся равным false
, т.к. не создано ни одной пользовательской
ссылки. Значение же "refcount" задаётся равным 1
, т.к. только одно имя переменной
указывает на данный контейнер. Обратите внимание, что ссылки (т.е. "is_ref" равно true
) с "refcount" равным 1
обрабатываются так, как если бы они не были ссылками (то есть как "is_ref" было бы false
). Если у вас установлен » Xdebug, то
вы можете вывести эту информацию, вызвав функцию xdebug_debug_zval().
Пример #2 Вывод информации о zval
<?php
$a = "new string";
xdebug_debug_zval('a');
?>
Результат выполнения данного примера:
a: (refcount=1, is_ref=0)='new string'
Присвоение этой переменной другой увеличивает счётчик ссылок.
Пример #3 Увеличение счётчика ссылок zval
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>
Результат выполнения данного примера:
a: (refcount=2, is_ref=0)='new string'
Счётчик ссылок здесь равен 2
, т.к.
a и b ссылаются на один
и тот же контейнер переменной. PHP достаточно умён, чтобы
не копировать контейнер, пока в этом нет необходимости. Как только
"refcount" станет равным нулю, контейнер уничтожается.
"refcount" уменьшается на единицу при уходе переменной
из области видимости (например, в конце функции) или при
удалении этой переменной (например при вызове unset()).
Пример #4 Уменьшение счётчика ссылок zval
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset( $c );
xdebug_debug_zval( 'a' );
?>
Результат выполнения данного примера:
a: (refcount=3, is_ref=0)='new string' a: (refcount=2, is_ref=0)='new string' a: (refcount=1, is_ref=0)='new string'
Если мы сейчас вызовем unset($a);
,
то контейнер, включая тип и значение, будет удалён из памяти.
Все несколько усложняется с составными типами данных, такими как массивы (array) и объекты (object). В отличие от скалярных (scalar) значений, массивы и объекты хранят свои свойства в собственных таблицах имён. Это значит, что следующий пример создаст сразу три zval контейнера:
Пример #5 Создание array zval
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
?>
Результатом выполнения данного примера будет что-то подобное:
a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42 )
Графически:
Результат - три контейнера: a, meaning и number. Похожие правила применяются и для увеличения и уменьшения "refcounts". Ниже мы добавляем ещё один элемент массива и устанавливаем ему значение уже существующего элемента:
Пример #6 Добавление уже существующего элемента в массив
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );
?>
Результатом выполнения данного примера будет что-то подобное:
a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=2, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42, 'life' => (refcount=2, is_ref=0)='life' )
Графически:
Вышеприведённый вывод Xdebug показывает, что и старый и новый элементы массива
сейчас указывают на контейнер, чей "refcount" равен 2
.
Хотя показано два контейнера со значением 'life'
, на самом
деле это один контейнер. Функция xdebug_debug_zval() не выводит информации об этом,
но вы можете проверить это также отобразив указатели памяти.
Удаление элемента из массива происходит точно так же, как и удаление имени переменной из области видимости: уменьшается "refcount" контейнера, на который ссылается элемент массива. Опять же, при достижении "refcount" нуля, контейнер удаляется из памяти. Пример:
Пример #7 Удаление элемента из массива
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
?>
Результатом выполнения данного примера будет что-то подобное:
a: (refcount=1, is_ref=0)=array ( 'life' => (refcount=1, is_ref=0)='life' )
Ситуация станет интереснее, если добавить массив новым элементом в самого себя. В следующем примере мы также используем оператор присваивания по ссылке, чтобы PHP не создал копию массива.
Пример #8 Добавление массива новым элементом в самого себя
<?php
$a = array( 'one' );
$a[] =& $a;
xdebug_debug_zval( 'a' );
?>
Результатом выполнения данного примера будет что-то подобное:
a: (refcount=2, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=... )
Графически:
Можно увидеть, что переменная с массивом (a), так же как и второй элемент
(1) сейчас указывают на контейнер с "refcount" равным 2
.
Символы "..." в выводе означают рекурсию и, в нашем случае,
указывают на оригинальный массив.
Как и ранее, удаление переменной уменьшает счётчик ссылок контейнера на единицу. Если мы применим unset к переменной $a после вышеприведённого примера, то счётчик ссылок контейнера, на который указывают $a и элемент "1", изменится с "2" на "1":
Пример #9 Удаление $a
(refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... )
Графически:
Хотя во всех областях видимости больше нет имени переменной, ссылающейся на данную структуру, она не может быть очищена, т.к. элемент массива "1" по-прежнему ссылается на этот массив. Т.к. теперь нет никакой возможности пользователю удалить эти данные, то мы получили утечку памяти. К счастью, PHP удалит эти данные при завершении запроса, но до этого момента данные будут занимать ценное место в памяти. Такая ситуация часто бывает, когда реализуются алгоритмы парсинга или другие, где есть дочерние элементы, ссылающиеся на родительские. Ещё чаще такая ситуация случается с объектами, потому что они всегда неявно используются по ссылке.
Эта не проблема, если такое случается раз или два, но если существуют тысячи или даже миллионы таких утечек памяти, то они уже становятся проблемой. Особенно в долгоработающих скриптах, таких как демоны, где запрос не заканчивается никогда, или в больших наборах модульных тестов. Последний случай вызвал проблемы при запуске модульных тестов для компонента Template из библиотеки ez Components. В некоторых случаях может потребоваться свыше 2 Гб памяти, которая не всегда есть на тестовом сервере.