Основи підрахунку посилань
Змінна PHP зберігається в контейнері, що називається zval. Контейнер zval, крім типу та значення змінної, також містить два додаткові біти інформації. Перший називається is_ref і представляє логічне значення, що вказує, включена змінна в "набір посилань" чи ні. За рахунок елемента is_ref PHP знає, як відрізняти звичайні змінні від посилань. Оскільки PHP дозволяє користувацькі посилання, які можна створити оператором &, контейнер zval також містить внутрішній механізм підрахунку посилань для оптимізації роботи пам'яті. Друга частина додаткової інформації називається refcount (лічильник посилань) і містить кількість імен змінних (або інша назва — символів), які вказують на цей контейнер zval. Усі імена змінних зберігаються у таблиці імен, окремої кожної області видимості змінних. Така область видимості існує для головного скрипта, кожної функції та методу.
Контейнер zval створюється при оголошенні нової змінної, якій надається константне значення, наприклад:
Приклад #1 Створення нового контейнера zval
Loading...
У цьому прикладі створюється новий символ a
у поточній області видимості та новий контейнер змінної з типом string та значенням new string
Бит is_ref за замовчуванням задається рівним false
, т. до. не створено жодного користувача посилання. Значення refcount задається рівним тільки одне ім'я змінної вказує на цей контейнер. Зверніть увагу, що посилання (тобто is_ref одно true
) з refcount рівним обробляються так, якби вони не були посиланнями (тобто як is_ref було б false
). Если установлен» Xdebug, можете вивести цю інформацію, викликавши функцію xdebug_debug_zval()
Приклад #2 Виведення інформації про zval
Loading...
Результат виконання наведеного прикладу:
a: (refcount=1, is_ref=0)='new string'
Присвоєння цієї змінної інший збільшує лічильник посилань.
Приклад #3 Збільшення лічильника посилань zval
Loading...
Результат виконання наведеного прикладу:
a: (refcount=2, is_ref=0)='new string'
Лічильник посилань тут дорівнює , Так як a і b посилаються на один і той же контейнер змінної. PHP досить розумний, щоб не копіювати контейнер, поки в цьому немає потреби. Як тільки refcount стане рівним нулю, контейнер знищується. refcount зменшується на одиницю при відході змінної з області видимості (наприклад, наприкінці функції) або при видаленні цієї змінної (наприклад при виклику) unset()
Приклад #4 Зменшення лічильника посилань zval
Loading...
Результат виконання наведеного прикладу:
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
Loading...
Висновок наведеного прикладу буде схожим на:
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 Додавання вже існуючого елемента масив
Loading...
Висновок наведеного прикладу буде схожим на:
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 дорівнює Хотя показано два контейнера со значением'life'
але це один контейнер. Функція xdebug_debug_zval() не виводить інформацію про це, але ви можете перевірити це також відобразивши покажчики пам'яті.
Елемент видаляється з масиву аналогічно видаленню імені змінної з області видимості: зменшується refcount-контейнер, на який посилається елемент масиву. При досягненні нуля в біті refcount, контейнер видаляється з пам'яті. Приклад:
Приклад #7 Видалення елемента з масиву
Loading...
Висновок наведеного прикладу буде схожим на:
a: (refcount=1, is_ref=0)=array (
'life' => (refcount=1, is_ref=0)='life'
)
Ситуація стане цікавішою, якщо додати масив новим елементом у самого себе. У наступному прикладі використано оператора присвоювання за посиланням, щоб PHP не створив копію масиву.
Приклад #8 Додавання масиву новим елементом до самого себе
Loading...
Висновок наведеного прикладу буде схожим на:
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
Графічно:
Можна побачити, що змінна з масивом (a), як і другий елемент (1) зараз вказують на контейнер з refcount рівним . Символи «...» у висновку означають рекурсію і, у разі, вказують на оригінальний масив.
Як і раніше, видалення змінної зменшує лічильник посилань контейнера на одиницю. Якщо застосувати конструкцію 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 ГБ пам'яті, яка не завжди є на тестовому сервері.