26.11.2016   3448

Управление памятью в PHP: циклические ссылки и сборщик мусора

Циклические ссылки и сборщик мусора в PHP

Всем привет! В предыдущих статьях (1, 2) мы рассмотрели структуру zval, разобрались как именно происходит присваивание и передача в функцию, рассмотрели разницу для разных типов. В предыдущих уроках мы рассматривали ситуации, когда вместе с удалением всех имён для какого-либо значения происходило уменьшение числа ссылок на значение до 0. В результате чего значение благополучно удалялось. Однако так происходит далеко не всегда, и об этих ситуациях мы сегодня и поговорим.

Для начала хотелось бы познакомить вас с функцией xdebug_debug_zval($var_name);

Она становится доступной после включения расширения Xdebug.
Рассмотрим следующий пример:

<?php
$a = 123;
$b = $a;
xdebug_debug_zval('a');

Результат будет следующим:

a: (refcount=2, is_ref=0)=123

Помните, в прошлых статьях мы говорили о полях структуры zval, одно из которых отвечает за количество ссылок на значение (не ссылок через &, а связей между именем и значением), а второе говорит о том, используется ли значение по ссылке (здесь уже в том смысле, когда мы используем &).

Как Вы уже поняли, первое из них это — refcount, а второе — is_ref. Отныне будем использовать именно эти названия. Так вот эта функция выводит эти значения в результате передачи в неё имени переменной, имя передаётся строкой. В нашем случае - 'a'. Теперь, используя эту функцию, Вы можете самостоятельно проверить все результаты, полученные нами в предыдущих двух статьях. Настоятельно рекомендую Вам это проделать. Уверен, Вам уже интересно проверить не только то, что мы разбирали ранее, желаю Вам удачи в Ваших экспериментах ;)

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

Так вот о чём это я начал говорить в начале. Как возможна ситуация, когда при удалении всех имён, ссылающихся на значение, число ссылок на это значение останется больше 0 и значение продолжит оставаться в памяти.
Напишем код, реализующий следующее: добавить в массив новым элементом самого себя и вывести получившееся значение zval.
Получится следующее:

<?php
$a = [];
$a[] = &$a;
xdebug_debug_zval('a');

Результат вывода:

a: (refcount=2, is_ref=1)=array (0 => (refcount=2, is_ref=1)=…)

Теперь, если мы сделаем unset($a), refcount уменьшится до 1, значение перестанет быть доступным нам по какому-либо имени и повиснет в памяти. К сожалению, проверить это с помощью только что ставшей известной для нас функции невозможно — имени-то нет. Попробуем для проверки этого заявления прибегнуть к изученной нами ранее функции memory_get_usage(). Для этого поместим в массив элемент, представляющий из себя строку из 100 000 символов. Этого будет достаточно, чтобы заметить изменения в памяти. Для создания такой строки прибегнем к функции str_repeat(). Код получится следующим:

<?php
$a = [];
echo (int)(memory_get_usage() / 1024) . ' KB' . PHP_EOL;
$a[] = str_repeat('a', 100000);
$a[] = &$a;
echo (int)(memory_get_usage() / 1024) . ' KB' . PHP_EOL;
unset($a);
echo (int)(memory_get_usage() / 1024) . ' KB' . PHP_EOL;

Результат:

123 KB
220 KB
220 KB

Как мы видим, память не освободилась. Попробуем не добавлять в массив элемент, ссылающийся на этот массив:

<?php
$a = [];
echo (int)(memory_get_usage() / 1024) . ' KB' . PHP_EOL;
$a[] = str_repeat('a', 100000);
echo (int)(memory_get_usage() / 1024) . ' KB' . PHP_EOL;
unset($a);
echo (int)(memory_get_usage() / 1024) . ' KB' . PHP_EOL;

Результат:

123 KB
220 KB
122 KB

Здесь мы видим, что размер запрашиваемой скриптом памяти вернулся к начальному значению. В первом же случае этого не произошло. Так как мы при этом не можем удалить эти «неиспользуемые» данные, то здесь имеет место быть утечка памяти. Примеры, подобные данному встречаются в коде довольно часто. Особенно когда речь идёт об объектах — во многих местах происходит их неявное использование по ссылке, в результате чего один объект в одном из своих свойств может начать содержать ссылку на себя самого.

Как правило, PHP используется непосредственно «для сайтов» и после завершения запроса эти данные будут удалены. Если такая утечка произойдёт в паре мест, то в большинстве случаев ничего ужасного не произойдёт. Однако если это долгоживущий скрипт, запущенный, например в CLI-режиме, то это может привести к выжиранию всей доступной памяти. К счастью, в PHP есть сборщик мусора, или по-английски "garbage collector".

Сборщик мусора (garbage collector)

По умолчанию сборщик мусора всегда включён. Это задаётся директивой zend.enable_gc в файле php.ini.
Он вызывается… На самом деле это довольно долго и сложно объяснять. Для тех, кто всё же хочет узнать прямо сейчас — прошу сюда.
Так вот, скажу я Вам, он сам прекрасно знает, когда приходит его время. Мы же остановимся на практическом варианте и посмотрим на него в действии. Итак, этот механизм вызывается при определённых условиях и занимается тем, что удаляет ненужные значения, возникшие в результате работы с циклическими ссылками. Напишем код, в бесконечном цикле которого будут создаваться объекты, содержащие ссылки на самих себя с последующим удалением их имён. На каждой тысячной итерации будем выводить размер используемой памяти. Код:

<?php
$i = 0;
while (true) {
    $obj = new stdClass();
    $obj->foo = $obj;
    if ($i++ % 1000 === 0) {
        echo (int)(memory_get_usage() / 1024) . ' KB' . PHP_EOL;
    }
}

Результат:

603 KB
790 KB
978 KB
1165 KB
1353 KB
1540 KB
1728 KB
1915 KB
2103 KB
2290 KB
603 KB
...

Как мы видим, размер используемой памяти некоторое время возрастает, а затем возвращается к исходному — это сборщик мусора в деле. Круто, да?
А теперь сделаем так, чтобы объект содержал в себе приличный объём данных:

<?php
$i = 0;
while (true) {
    $obj = new stdClass();
    $obj->foo = $obj;
    $obj->bar = str_repeat('a', 100000);
    if ($i++ % 1000 === 0) {
        echo (int)(memory_get_usage() / 1024) . ' KB' . PHP_EOL;
    }
}

Вывод:

799 KB
98736 KB
196674 KB
294611 KB
392549 KB
490486 KB
588424 KB
686361 KB
784299 KB
882236 KB
799 KB
98736 KB

Вроде бы всё хорошо, однако, если Вы запустите этот скрипт у себя, то увидите ощутимые фризы в работе в момент работы garbage collector’а, где-то на секунду выполнение скрипта остановится. Это время напрямую зависит от объема высвобождаемой памяти. И в предыдущем варианте, где объекты были довольно малы работа garbage collector’а на первый взгляд незаметна. Однако, работа скрипта всё же будет на некоторое время остановлена. В этом его минус, за всё приходится платить. Как правило, Вы вряд ли от этого пострадаете, однако, об этом следует помнить, особенно при работе с большими данными. Если есть фризы и для Вас это критично — Вы знаете в какую сторону теперь копать.

А вот теперь все дружно идём в документацию и читаем про то, когда же всё-таки будет вызван сборщик мусора - Сбор циклических ссылок

Спасибо за прочтение, желаю Вам всего хорошего ;)

Ссылки на статьи этой серии:
Управление памятью в PHP. Начало
Управление памятью в PHP. Передача в функцию, классы, массивы
>Управление памятью в PHP. Циклические ссылки и сборщик мусора

Комментарии

 Pti4ka 2017-05-02 06:58:06
0

когда планируете выложить курс дальше?

Ответить
  •  ivashkevich 2017-05-02 07:55:33
    0

    Приветствую, был в отпуске, на этой неделе продолжу.

    Ответить
Чтобы написать комментарий нужно войти на сайт.
Или получить доступ прямо сейчас:


Нажимая эту кнопку
Вы принимаете политику конфиденциальности