Снижение потребления памяти в 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 для строк и записывать напрямую в файл.