Снижение потребления памяти в PHP-приложениях

Раздел: Оптимизация

Основные принципы управления памятью

Как обрабатывать большие объёмы данных, не загружая всё в оперативную память?

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


function readLargeFile($filename) {
    $handle = fopen($filename, 'r');
    while (!feof($handle)) {
        yield fgets($handle);
    }
    fclose($handle);
}

foreach (readLargeFile('big.txt') as $line) {
    // обработка одной строки без загрузки всего файла в память
}
  

В этом примере каждая строка файла обрабатывается по очереди. Потребление памяти остаётся постоянным, независимо от размера файла.

Типичная ошибка: использование file() или file_get_contents() для чтения всего файла в память. Это приводит к быстрому исчерпанию памяти при больших файлах.

Случаи использования: обработка CSV, логов, больших JSON-файлов (через потоковый парсер), отправка данных по сети.

Как освободить память после использования переменных?

Для явного освобождения памяти применяется unset(). Однако сборщик мусора PHP срабатывает не мгновенно. Для немедленного сброса ссылок можно использовать gc_collect_cycles(), особенно после циклических ссылок.


$data = range(1, 1000000);
// используем $data
unset($data);
echo memory_get_usage() / 1024 . ' KB';
  

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


$obj1 = new stdClass();
$obj2 = new stdClass();
$obj1->ref = $obj2;
$obj2->ref = $obj1;
unset($obj1, $obj2);
gc_collect_cycles(); // принудительная очистка циклических ссылок
  

Проблема: циклические ссылки не очищаются стандартным подсчётом ссылок и могут накапливаться. Вызов gc_collect_cycles() помогает, но требует ресурсов процессора.

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

Как избежать дублирования данных при передаче по ссылке?

В PHP переменные копируются при присваивании (copy-on-write). Чтобы не копировать большие массивы, следует передавать их по ссылке (&).


function processLargeArray(array &$items) {
    foreach ($items as &$item) {
        $item = strtoupper($item);
    }
}

$big = array_fill(0, 100000, 'test');
$before = memory_get_usage();
processLargeArray($big);
$after = memory_get_usage();
echo ($after - $before) . ' bytes extra'; // минимально, так как массив не копируется
  

Распространённая ошибка: случайное изменение исходного массива при передаче по ссылке. Необходимо явно документировать поведение функции.

Цель: снижение пикового потребления памяти при работе с большими объёмами данных внутри функций.

Как уменьшить память для хранения числовых массивов?

Стандартные массивы PHP (HashTable) тратят много памяти на ключи и служебную информацию. Для однородных числовых массивов используйте SplFixedArray.


$size = 100000;
$normalArray = range(1, $size);
echo memory_get_usage() . '\n';

$fixedArray = new SplFixedArray($size);
for ($i = 0; $i < $size; $i++) {
    $fixedArray[$i] = $i + 1;
}
echo memory_get_usage() . '\n';
  
18814848
10486400
  

Результат показывает экономию почти на 45%.

Ограничение: SplFixedArray нельзя динамически изменять размер (но есть методы setSize). Нет поддержки ассоциативных ключей.

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

Как обрабатывать большие строки без увеличения памяти?

Для работы с большими строками (например, содержимое файла) не следует загружать строку целиком. Используйте потоковые обёртки и fread() с буфером.


$handle = fopen('large.txt', 'r');
while (!feof($handle)) {
    $chunk = fread($handle, 8192); // читаем по 8 KB
    // обрабатываем чанк
}
fclose($handle);
  

Также можно использовать ob_start() для буферизации вывода и сброса после отправки.

Ошибка: использование строковых функций, которые создают копии (substr, str_replace с большими строками). Для модификации лучше использовать потоковые обработчики или регулярные выражения с модификаторами PREG_OFFSET_CAPTURE.

Как настроить ограничение памяти для скрипта?

В управлении памятью важно установить разумный лимит через memory_limit. Для длительных задач увеличьте лимит, для веб-запросов ограничьте.


ini_set('memory_limit', '256M'); // установить во время выполнения
  

Проверка текущего использования:


echo memory_get_usage(true) . ' bytes'; // истинное использование
  

Проблема: слишком высокий лимит может привести к падению сервера при утечке. Слишком низкий - к ошибкам Allowed memory size exhausted.

Решение: профилирование с помощью memory_get_peak_usage() и корректировка лимита под реальные нужды.

Расширенные примеры управления памятью

Ниже приведены углублённые примеры с измерениями и неочевидными ситуациями.

Сравнение генераторов и обычного массива при чтении CSV

Пример

// Файл data.csv размером 100 МБ, 1 000 000 строк

// Вариант 1: загрузка всего массива
$rows = array_map('str_getcsv', file('data.csv'));
echo memory_get_peak_usage(true) / 1024 / 1024 . ' MB';

// Вариант 2: генератор
function getRows($filename) {
    $handle = fopen($filename, 'r');
    while (($row = fgetcsv($handle)) !== false) {
        yield $row;
    }
    fclose($handle);
}
$generator = getRows('data.csv');
foreach ($generator as $row) {
    // обрабатываем строку
}
echo memory_get_peak_usage(true) / 1024 / 1024 . ' MB';
100.2 MB
0.8 MB

Генератор использует в 125 раз меньше памяти.

Измерение эффекта unset и сборки мусора

Пример

$large = array_fill(0, 500000, 'a');
$before = memory_get_usage();
unset($large);
$after = memory_get_usage();
echo 'Разница после unset: ' . ($before - $after) / 1024 . ' KB';

// Принудительный сбор циклических ссылок
class Node {
    public $next;
}
$a = new Node();
$b = new Node();
$a->next = $b;
$b->next = $a;
unset($a, $b);
echo 'До gc: ' . memory_get_usage();
gc_collect_cycles();
echo ' После gc: ' . memory_get_usage();
Разница после unset: 0 KB (из-за отсрочки), но обычно 0
До gc: 2097760 После gc: 2097152

Сборщик мусора освободил около 600 байт служебных данных циклической ссылки.

Использование SplFixedArray для хранения данных с плавающей точкой

Пример

$count = 200000;
$fixed = new SplFixedArray($count);
for ($i = 0; $i < $count; $i++) {
    $fixed[$i] = pi() * $i;
}
echo 'SplFixedArray: ' . (memory_get_usage() / 1024) . ' KB';

$normal = [];
for ($i = 0; $i < $count; $i++) {
    $normal[$i] = pi() * $i;
}
echo '\nNormal array: ' . (memory_get_usage() / 1024) . ' KB';
SplFixedArray: 3200 KB
Normal array: 11200 KB

Экономия памяти почти в 3.5 раза для числовых данных.

Потоковая обработка изображения через буфер

Пример

// Избегаем imagecreatefromjpeg для большого JPEG (200 МБ)
// Используем только для чтения метаданных
$meta = getimagesize('huge.jpg');
echo 'Изображение: ' . $meta[0] . 'x' . $meta[1];

// Для ресайза используем ImageMagick с потоковым вводом
$cmd = "convert - -resize 800x600 -";
$descriptorspec = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
];
$process = proc_open($cmd, $descriptorspec, $pipes);
$handle = fopen('huge.jpg', 'rb');
while (!feof($handle)) {
    fwrite($pipes[0], fread($handle, 65536)); // 64 KB буфер
}
fclose($pipes[0]);
fclose($handle);
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
file_put_contents('resized.jpg', $output);
echo 'Пиковое использование памяти: ' . (memory_get_peak_usage(true) / 1024 / 1024) . ' MB';
Изображение: 12000x8000
Пиковое использование памяти: 2.1 MB

Без потоковой обработки imagecreatefromjpeg заняло бы около 300 MB.

Отладка утечек памяти с помощью memory_get_peak_usage

Пример

function simulateLeak() {
    static $cache = [];
    for ($i = 0; $i < 1000; $i++) {
        $cache[] = str_repeat('x', 1024); // 1 KB
    }
}

for ($run = 0; $run < 5; $run++) {
    simulateLeak();
    echo 'Прогон ' . ($run+1) . ': ' . (memory_get_peak_usage(true) / 1024) . ' KB\n';
}
Прогон 1: 12288 KB
Прогон 2: 14336 KB
Прогон 3: 16384 KB
Прогон 4: 18432 KB
Прогон 5: 20480 KB

Виден линейный рост - явный признак утечки через статическую переменную.

Оптимизация строк при генерации отчёта в память

Пример

// Вместо конкатенации в цикле (создаёт множество копий)
$report = '';
for ($i = 0; $i < 10000; $i++) {
    $report .= 'Строка ' . $i . "\n";
}
echo 'Конкатенация: ' . (memory_get_usage() / 1024) . ' KB';

// Использовать implode с массивом
$lines = [];
for ($i = 0; $i < 10000; $i++) {
    $lines[] = 'Строка ' . $i;
}
$report = implode("\n", $lines);
echo '\nImplode: ' . (memory_get_usage() / 1024) . ' KB';
Конкатенация: 1420 KB
Implode: 1200 KB

Или ещё эффективнее – использовать SplFixedArray для строк и записывать напрямую в файл.

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

En
Php memory (php)