Основы подсчета ссылок

Переменная 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, т.к. только одно имя переменной указывает на данный контейнер. Отметим, что если "refcount" равен 1, то "is_ref" будет всегда равен FALSE. Если у вас установлен » Xdebug, то вы можете вывести эту информацию, вызвав функцию xdebug_debug_zval().

Пример #2 Вывод информации о zval

<?php
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 Гб памяти, которая не всегда есть на тестовом сервере.