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

Переменная PHP хранится в контейнере, который называется zval (англ. сокр.: Zend Value — Zend-значение). Контейнер zval, кроме типа и значения переменной, также содержит два дополнительных бита информации. Первый называется is_ref (англ. сокр.: Is Reference — ссылка ли?) и представляет логическое значение, которое указывает, включил ли PHP переменную в «набор ссылок» или нет. За счёт бита is_ref PHP-движок знает, как отличать обычные переменные от ссылок. Поскольку PHP разрешает пользовательские ссылки, которые создают оператором &, контейнер zval также содержит внутренний механизм подсчёта ссылок для оптимизации работы памяти. Вторая часть дополнительной информации называется refcount (англ. сокр.: Reference Counter — счётчик ссылок) и содержит количество имён переменных, или другое название — символов, которые указывают на этот zval-контейнер. Каждый символ хранится в таблице символов. У каждой области видимости переменных своя таблица символов. PHP создаёт отдельную область видимости для главного скрипта, который срабатывает при запросе из браузера, и отдельную область видимости для каждой функции или метода.

Контейнер zval создаётся при объявлении новой переменной, которой присваивается константное значение, например:

Пример #1 Создание нового контейнера zval

<?php

$a
= "new string";

?>

В примере в текущей области видимости создаётся новый символ с именем "a" и новый контейнер переменной с типом string и значением new string. Бит is_ref по умолчанию задаётся равным false, поскольку не создали ни одной пользовательской ссылки. Значение же бита refcount задаётся равным 1, поскольку с этим контейнером PHP связал только один символ. Обратите внимание, что ссылки — zval-контейнеры с битом 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, поскольку с одним и тем же контейнером переменной PHP связал как символ a, так и символ b. PHP достаточно умён, чтобы не копировать сам контейнер, пока этого не требуется. Как только счётчик ссылок refcount становится равным нулю, контейнеры переменных уничтожаются. Счётчик ссылок refcount уменьшается на единицу, когда символ, который PHP связал с контейнером переменной, выходит из области видимости (например, в конце функции), или при удалении символа (например, при вызове языковой конструкции 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 Создание zval-контейнера для массива (array)

<?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
)

Графически:

Контейнеры для простого массива

PHP создаёт три zval-контейнера для символов: a, meaning и number. Аналогичные правила применяются для увеличения и уменьшения количества ссылок. В следующем примере в массив добавляется ещё один элемент, которому устанавливается значение другого элемента массива:

Пример #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 видно, что как старый, так и новый элемент массива теперь указывает на zval-контейнер, значение refcount которого равно 2. Хотя вывод модуля Xdebug показывает два zval-контейнера со значением '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 ГБ памяти, доступа к которой на тестовом сервере не было.

Добавить

Примечания пользователей 6 notes

up
17
Anonymous
10 years ago
If a variable is not present in the current scope xdebug_debug_zval will return null.
up
13
Anonymous
10 years ago
There seems to be no way to inspect the reference count of a specific class variable but you can view the reference count of all variables in the current class instance with xdebug_debug_zval('this');
up
7
skymei at skymei dot cn
5 years ago
$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');

ouputs with PHP 7.3.12 (cli)

a: (interned, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1
up
7
shkarbatov at gmail dot com
6 years ago
Result of "Example #8 Adding the array itself as an element of it self" will be another for PHP7:

a: (refcount=2, is_ref=1)=array (
0 => (refcount=2, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)

insted of:
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)

Internal value representation in PHP 7:
https://nikic.github.io/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html
up
4
yuri1308960477 at gmail dot com
5 years ago
my php versoin : HP 7.1.25 (cli) (built: Dec 7 2018 08:20:45) ( NTS )

$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');

output:

a: (refcount=2, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1

if $a is a string value, 'refcount' equal 2 by defalut.
up
0
chxt2011 at 163 dot com
5 years ago
my php version is PHP 7.1.6 (cli), when I run

$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');

it shows:
a: (refcount=0, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1
To Top