19.11.2016   3936

Управление памятью в PHP: начало

Управление памятью в PHP

Всем привет. Сегодня разговор пойдёт о том, каким образом в PHP реализовано хранение данных в памяти и о том, что же на самом деле скрывает за собой довольно простой код. В данной статье я хотел бы больше показать на практике результаты работы разного кода, опуская многие детали внутренней реализации. На мой взгляд, для разработчика на языке PHP более приоритетны знания о том, как писать код на PHP, нежели всё внутреннее устройство интерпретатора. Ну а заинтересовавшиеся данной темой люди легко нагуглят материалы по данной теме. Итак, приступим.

Функции для анализа памяти

В первую очередь стоит познакомиться с некоторыми функциями, позволяющими узнать об использовании памяти. Начнём с функции memory_get_usage($real_usage = null).

Эта функция возвращает количество памяти в байтах, выделенной скрипту в данный момент. По умолчанию (без передачи параметров), она возвращает количество памяти, запрошенное процессом для его работы:

echo memory_get_usage();
352952

При передаче ей единственно возможного аргумента типа boolean, равного true, функция вернёт реально выделенное количество памяти, которое всегда больше, чем запрошенное значение:

echo memory_get_usage(true);
2097152

Поиграемся немного с этими функциями, напишем небольшой скрипт, который будет сначала забивать память какими-либо данными, а потом уничтожать их:

<?php
function echoMemoryUsage() {
    echo 'Requested: ' . (int)(memory_get_usage() / 1024) . ' KB';
    echo PHP_EOL . '';
    echo 'Allocated: ' . (int)(memory_get_usage(true) / 1024) . ' KB';
    echo PHP_EOL . PHP_EOL;
}

echoMemoryUsage();

// Создадим строку из 10 млн. символов 'f'
$bigString = str_repeat('f', 10000000);

echoMemoryUsage();

// Удалим переменную
unset($bigString);

echoMemoryUsage();

Результат выполнения данного скрипта:

Requested: 343 KB
Allocated: 2048 KB

Requested: 10583 KB
Allocated: 12288 KB

Requested: 343 KB
Allocated: 2048 KB

Как мы видим, в процессе работы скрипта количество запрашиваемой и выделенной памяти сначала увеличилось, а затем вернулось в исходное состояние.

Рассмотрим также функцию memory_get_peak_usage($real_usage = null)
Она возвращает максимальный объем памяти в байтах, который был выделен PHP скрипту за всё прошедшее время выполнения до данного момента. Как и у предыдущей рассмотренной нами функции у неё есть единственный аргумент, имеющий такой же смысл. Дополним наш скрипт вызовом этой функции в конце:

<?php
function echoMemoryUsage() {
    echo 'Requested: ' . (int)(memory_get_usage() / 1024) . ' KB';
    echo PHP_EOL . '';
    echo 'Allocated: ' . (int)(memory_get_usage(true) / 1024) . ' KB';
    echo PHP_EOL . PHP_EOL;
}

echoMemoryUsage();

// Создадим строку из миллиона символов 'f'
$bigString = str_repeat('f', 10000000);

echoMemoryUsage();

unset($bigString);

echoMemoryUsage();

echo 'Peak requested: ' . (int)(memory_get_peak_usage() / 1024) . ' KB';
echo PHP_EOL;
echo 'Peak allocated: ' . (int)(memory_get_peak_usage(true) / 1024) . ' KB';

Теперь вывод будет таким:

Requested: 344 KB
Allocated: 2048 KB

Requested: 10584 KB
Allocated: 12288 KB

Requested: 344 KB
Allocated: 2048 KB

Peak requested: 10584 KB
Peak allocated: 12288 KB

Функция отработала ожидаемым образом. Её можно использовать в конце работы скрипта как вариант быстрого анализа эффективности кода по памяти.

Что происходит на самом деле?

Итак, мы научились узнавать об использовании памяти. Рассмотрим теперь более детально следующий код:

$var = 123;
unset($var);

Чтобы понять, как он работает, нам всё же придётся немного углубиться во внутреннюю реализацию языка PHP. Все значения переменных являются структурой zval. Структура zval состоит из четырех полей. Не будем заострять своё внимание на том, как именно это всё устроено. Нам лишь достаточно знать о том, что это за поля, и для чего они нужны. Итак, эти поля:

  • значение;
  • тип этого значения;
  • число ссылок на это значение;
  • флаг того, что данное значение используется по ссылке (подробности позже, пока не заморачивайтесь).

В первой строке нашего кода происходит создание переменной. В этот момент в куче выделяется память под значение, создаётся структура zval и заполняется указанным значением 123. Затем в специальном месте, называемом таблицей символов, создаётся имя данной переменной $var. Наконец, устанавливается связь между именем переменной и значением в памяти. Теперь переменная $var ссылается на значение 123.

В данный момент поля структуры zval имеют следующие значения:

  • тип: целое число
  • значение: 123
  • число ссылок: 1
  • является ли значение использующимся по ссылке: нет, или 0 (подробности позже).

Что же происходит, когда мы вызываем unset($var)? В таблице символов удаляется имя $var, после чего в структуре zval в поле число ссылок устанавливается значение 0. Таким образом, на это значение больше не ссылается ни одно имя. PHP, видя это, уничтожает данное значение, освобождая таким образом занимаемую им память.
Что произойдёт, если вместо unset, мы присвоим данной переменной значение null? Произойдёт разрыв связи между именем $var и её значением. $var перестаёт ссылаться на какое-либо значение. Теперь $var – это просто имя, и ничего кроме. Число ссылок на значение в этом случае тоже станет равно 0 и оно будет уничтожено.

Ссылки

Рассмотрим следующий код:

$var1 = 123;
$var2 = $var1;
$var1 = 12345;

В первой строке мы создаём переменную и присваиваем ей значение. Тут всё понятно, это мы проходили.
На следующей строке переменной $var2 присваивается значение $var1. Однако, внутри не всё так просто, как кажется. Мы полагаем, что в куче будет выделена память, значение скопировано, и новое имя начнёт указывать на новое созданное значение. Это не так :)
На самом деле, после выполнения второй строки имя $var2 будет указывать всё ещё на тот же zval. При этом у этого zval в поле число ссылок теперь будет число 2. Прошу обратить внимание, что под ссылками мы сейчас подразумеваем не ссылки PHP, которые начинаются со знака &, а связь между именем и значением. Больше никаких изменений в этом zval на данный момент не произойдёт.
Перейдём к третьей строке. А вот теперь будет создан новый zval! Назовём его zval2, а предыдущий — zval1. Рассмотрим теперь, что же именно произошло. А произошло следующее: была выделена память под значение 12345 и создан новый zval (zval2) с этим значением, имя $var1 теперь ссылается на zval2. $var2 продолжает ссылаться на zval1, число ссылок на zval1 уменьшилось и теперь равняется 1. Этот механизм называется copy-on-write. Благодаря ему PHP создаёт новое значение только в тот момент, когда это действительно необходимо. Надеюсь, тут всё понятно.

Теперь рассмотрим следующий код:

$var1 = 123;
$var2 = &$var1;
$var1 = 12345;
echo $var2;

и результат его работы:

12345

Рассмотрим вторую строку. Благодаря символу & переменной $var2 происходит присваивание по ссылке значения переменной $var1. В результате этого появляется лишь ещё одно имя, ссылающееся на то же значение. И теперь пришла пора поговорить о четвёртом поле в структуре zval, которое в примерах выше всегда было равно 0. Здесь-то оно и превращается в единицу. И значения полей полученного zval становятся следующими:

  • тип: целое число
  • значение: 123
  • число ссылок: 2
  • является ли значение использующимся по ссылке: да (1).

Благодаря этому флагу PHP определяет, что этот zval не должен быть скопирован перед изменением, а должно быть изменено лишь его значение.
В результате этого, при выполнении кода в третьей строке, значение, на которое ссылается имя $var1 будет заменено на 12345. На это же значение ссылается имя $var2. В результате этого при чтении значения $var2 мы получим то же значение — 12345.
Теперь, если сделать unset($var1), значение не удалится, оно будет по-прежнему доступно по имени $var2. В zval при этом число ссылок уменьшится до 1, а поле, означающее что значение является использующимся по ссылке станет нулевым.

На этом первая часть истории про управление памятью в PHP заканчивается. Во второй части я расскажу о том, что происходит во время вызова функций, про разницу в передаче разных типов аргументов и про сборщик мусора в PHP. Желаю Вам всего наилучшего.

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

Комментарии

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


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