اولین بار که با Allowed memory size exhausted مواجه شدم، فکر کردم سرور مشکل دارد. چند ثانیه بعد فهمیدم که سرور کاملاً سالم است و مشکل در کد من است: یک حلقه که در هر تکرار، کل جدول دیتابیس را در حافظه می‌ریخت. آن روز، تفاوت بنیادین بین خطای Memory limit و خطای Timeout را یاد گرفتم. خطای Memory limit در PHP یک پیام واضح از سیستم است که می‌گوید شما از بودجه‌ی حافظه‌ی خودتان تجاوز کرده‌اید.

خطای Memory limit در PHP دقیقاً چیست؟

PHP به‌عنوان یک زبان سمت سرور، در محیطی اجرا می‌شود که منابع حافظه‌ی سرور بین ده‌ها یا صدها پروسه‌ی همزمان تقسیم می‌شود. برای جلوگیری از اینکه یک اسکریپت معیوب، تمام حافظه‌ی سرور را قفل کند، PHP یک محدودیت سقف مصرف حافظه دارد که به آن memory_limit می‌گویند. وقتی این محدودیت سر می‌رسد، PHP اسکریپت را متوقف می‌کند و پیام زیر را ثبت می‌کند:

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) in /path/to/file.php on line N

این خطا از نوع Fatal است - یعنی اسکریپت کاملاً متوقف می‌شود. تفاوت اصلی این خطا با خطای Maximum execution time این است که در خطای حافظه، مسئله‌ی زمان نیست، بلکه مقدار حافظه‌ای است که اسکریپت مصرف کرده. یک اسکریپت می‌تواند در 2 ثانیه حافظه‌ی سرور را پر کند، یا می‌تواند ساعتی بدون مشکل اجرا شود. تفاوت در نوع منابع مصرفی است، نه در زمان.

نکته‌ی مهم در پیام خطا، دو عدد است: مقدار مجاز و مقدار تلاش‌شده. اگر این دو عدد به هم نزدیک باشند، ممکن است فقط چند کیلوبایت کم داشته باشید. اگر فاصله‌شان زیاد باشد، اسکریپت شما یک دفعه به سراغ مصرف سنگین رفته. این تفکیک، سرنخ اولیه در تشخیص است.

مبانی کار با PHP را در آموزش PHP از صفر برای مبتدیان می‌توانید مرور کنید. آنجا مفهوم حافظه و مدیریت آن در سطح مبتدی توضیح داده شده است.

حافظه در PHP، شبیه به یک میز کار است: هرچه بزرگ‌تر، هزینه‌ی راه‌اندازی بیشتر. اگر همیشه از میز بزرگ استفاده کنید، هزینه‌ی نگهداری آن را می‌پردازید. اگر از میز کوچک استفاده کنید، به چابکی می‌رسید ولی باید کار را تکه‌تکه کنید.

چرا PHP محدودیت حافظه اعمال می‌کند؟

مثل محدودیت زمانی، محدودیت حافظه هم از سه دلیل بنیادین می‌آید:

یک: محافظت از سرور. یک اسکریپت معیوب که در هر درخواست 500 مگابایت حافظه مصرف کند، در عرض چند دقیقه سرور را از کار می‌اندازد. محدودیت حافظه، حصار محافظتی است.

دو: محافظت از سایر پروسه‌ها. اگر یک سایت از سرور اشتراکی تمام حافظه را بگیرد، بقیه‌ی سایت‌ها از کار می‌افتند. محدودیت حافظه، عادلانه بودن منابع را تضمین می‌کند.

سه: بازخورد سریع به توسعه‌دهنده. اگر بدون محدودیت، اسکریپت شما در حین اجرا سرور را کند کند بدون اینکه خطایی بدهد، شما هرگز نمی‌فهمید که کدتان مصرف سنگینی دارد. محدودیت حافظه، این را زودتر آشکار می‌کند.

پس وقتی این خطا ظاهر می‌شود، پیام اصلی این است: کد شما داده‌ی بیشتری از بودجه‌ی حافظه‌اش مصرف می‌کند. راه‌حل، نه حذف محدودیت است، نه چشم بستن به مصرف. راه‌حل درست، بازنگری در نحوه‌ی مدیریت داده است. این فلسفه، در مباحث بهینه‌سازی کدهای PHP به‌تفصیل باز شده است.

کجا این محدودیت تنظیم می‌شود؟

مانند محدودیت زمانی، مقدار memory_limit در چند لایه قابل تنظیم است:

فایل php.ini

این لایه، اصلی‌ترین و پایدارترین است:

memory_limit = 128M

مقدار پیش‌فرض معمولاً بین 128M و 256M است. روی هاست‌های اشتراکی، ممکن است 256M باشد، ولی روی سرورهای اختصاصی، بسته به پیکربندی، می‌تواند بالاتر یا پایین‌تر باشد.

فایل .htaccess

روی سرورهای Apache:

php_value memory_limit 256M

این روش، روی سرورهای PHP-FPM کار نمی‌کند. دام رایجی است که در پروژه‌های مهاجرت‌کرده دیده‌ام.

تابع ini_set()

در ابتدای اسکریپت:

ini_set("memory_limit", "512M");

این روش، انعطاف‌پذیر است، ولی در بعضی هاست‌ها ini_set در disable_functions قرار دارد و کار نمی‌کند. توصیه: قبل از اتکا به این روش، با یک تست ساده بررسی کنید.

دستور خط فرمان برای CLI

برای اسکریپت‌های CLI:

php -d memory_limit=512M script.php

این روش، در cron jobها و اسکریپت‌های مهاجرت مفید است. می‌توانید بدون تغییر کلی پیکربندی، یک اسکریپت را با محدودیت بالا اجرا کنید.

کد WP-CLI و وردپرس

در وردپرس، مقدار WP_MEMORY_LIMIT در wp-config.php تنظیم می‌شود. ولی این مقدار تنها در چارچوب وردپرس اعمال می‌شود و اگر ini_set محدود باشد، کار نمی‌کند. برای درک کلی وردپرس، وردپرس چیست نقطه‌ی شروع خوبی است.

چرا افزایش محدودیت، راه‌حل واقعی نیست؟

واکنش طبیعی به خطای Memory limit، افزایش مقدار است. این واکنش در کوتاه‌مدت، خطا را پنهان می‌کند، ولی در بلندمدت سه هزینه‌ی جدی ایجاد می‌کند:

یک: انباشت مصرف. اگر یک اسکریپت 500 مگابایت حافظه مصرف می‌کند، محدودیت را به 1 گیگ می‌برید. چند هفته بعد، اسکریپت در سناریوهای پیچیده به 900 مگ می‌رسد و دوباره خطا می‌دهد. این چرخه، بدون سقف ادامه پیدا می‌کند.

دو: فشار بر سایر پروسه‌ها. اگر سایت شما در سرور مشترک است، اسکریپتی که حافظه‌ی زیاد می‌گیرد، سهم سایر سایت‌ها را کم می‌کند. این رفتار در بلندمدت به تنش با هاست و در نهایت به تعلیق حساب منجر می‌شود.

سه: افت کارایی. مصرف بالای حافظه، معمولاً همراه با مصرف بالای CPU و I/O است. حتی اگر حافظه محدود نباشد، کارایی سایت به‌شدت افت می‌کند. معیارهای کارایی در بهینه‌سازی کدهای PHP به‌تفصیل آمده است.

پس رویکرد حرفه‌ای در برخورد با این خطا، تشخیص علت و بازنویسی کد است. افزایش محدودیت، تنها به‌عنوان یک راه‌حل موقت و صرفاً در سناریوهای خاص - مثل migration یک‌باره - قابل توجیه است. هر جای دیگری، این کار، پنهان کردن مشکل است نه حل آن. همین فلسفه در خطای Warning در PHP هم به‌کار می‌آید.

افزایش memory_limit مثل کشیدن یک تکه‌ی اضافه به کمربند است که هر ماه تنگ‌ترش می‌کنید. مشکل در کمربند نیست، در رژیم غذایی برنامه است.

شش علت رایج مصرف بالای حافظه

در تجربه‌ی من روی ده‌ها پروژه، خطای Memory limit از چند الگوی تکراری می‌آید. شناخت این الگوها، نیمی از تشخیص است.

علت اول: بارگذاری کل جدول در حافظه

کوئری SELECT * FROM large_table بدون محدودیت، تمام ردیف‌ها را در حافظه بارگذاری می‌کند. اگر جدول 10 میلیون ردیف داشته باشد، حتی اگر هر ردیف 100 بایت باشد، مجموع یک گیگابایت می‌شود.

علت دوم: حلقه‌های بازگشتی بدون محدودیت

حلقه‌ای که در هر تکرار، یک آرایه‌ی جدید به آرایه‌ی اصلی اضافه می‌کند، به‌سرعت حافظه را پر می‌کند. حتی اگر هر آرایه کوچک باشد، مجموع بزرگ می‌شود.

علت سوم: بارگذاری فایل بزرگ با file_get_contents

فراخوانی file_get_contents() روی یک فایل 500 مگابایتی، همان فایل را در حافظه بار می‌کند. راه‌حل: استفاده از fopen و fread به‌صورت قطعه‌ای. الگوی کامل در رفع خطای Failed to open stream آمده است.

علت چهارم: تصاویر و PDF بزرگ

کتابخانه‌های پردازش تصویر مثل GD، هر تصویر را در چند نسخه‌ی مختلف در حافظه نگه می‌دارند. یک تصویر 5000x5000 پیکسلی، می‌تواند در حافظه بیش از 100 مگابایت شود. پردازش 10 تصویر همزمان، به‌سرعت از یک گیگابایت عبور می‌کند.

علت پنجم: ساختارهای داده‌ی ناکارآمد

استفاده از آرایه‌های تودرتوی عمیق، یا حفظ داده‌های زیاد در متغیرهای سراسری، می‌تواند به‌سرعت حافظه را پر کند. مثال: نگه‌داشتن نتایج یک حلقه‌ی بزرگ در آرایه به‌جای پردازش فوری.

علت ششم: نشتی حافظه در عملیات تکراری

بعضی از عملیات - مثل فراخوانی متعدد یک API - می‌توانند به‌آرامی حافظه را مصرف کنند بدون اینکه آزاد کنند. این پدیده در کدهای بدون reference-counting صحیح، شایع است.

دام بزرگ: بارگذاری همه‌چیز در حافظه

شایع‌ترین علت Memory limit، الگوی بارگذاری همه‌چیز در حافظه است. این الگو در کدهای ناشی از آموزش‌های ابتدایی شایع است، چون در مثال‌های کوچک جواب می‌دهد.

مثال کلاسیک: خواندن یک فایل بزرگ CSV برای پردازش.

// الگوی معیوب - فایل کامل در حافظه
$content = file_get_contents("huge.csv");
$lines = explode("\n", $content);
foreach ($lines as $line) {
    process(explode(",", $line));
}

در این الگو، حتی اگر فایل 500 مگابایت باشد، حافظه‌ی مصرفی می‌تواند دو یا سه برابر شود، چون PHP هم فایل خام و هم آرایه‌ی خطوط را در حافظه نگه می‌دارد. راه‌حل، استفاده از stream:

// الگوی بهینه - خط به خط پردازش می‌شود
$handle = fopen("huge.csv", "r");
while (($row = fgetcsv($handle)) !== false) {
    process($row);
}
fclose($handle);

در این الگو، حافظه‌ی مصرفی در طول پردازش ثابت می‌ماند - معمولاً چند صد کیلوبایت - و می‌توانید فایل‌های چند گیگابایتی را پردازش کنید. این تکنیک در مباحث مربوط به کار با آرایه‌های PHP به‌تفصیل باز شده است.

آرایه‌های بزرگ و مدیریت داده

آرایه‌های بزرگ در PHP، گاهی چند برابر داده‌ی خام، حافظه مصرف می‌کنند. دلیل: هر عنصر آرایه شامل خود داده، کلید، و metadata مدیریتی است. برای یک آرایه‌ی ساده از اعداد، مصرف حافظه تقریباً 4 برابر آرایه‌ی معادل در زبان‌های سطح پایین‌تر است.

چند تکنیک برای کاهش مصرف حافظه آرایه‌ها:

استفاده از SplFixedArray به‌جای array

در مواردی که اندازه‌ی آرایه از قبل معلوم است، SplFixedArray حافظه‌ی کمتری مصرف می‌کند:

$array = new SplFixedArray(1000000);
$array[0] = "value";

این کلاس، برای آرایه‌های عددی بزرگ با اندازه‌ی معلوم، تفاوت چشمگیری در مصرف حافظه دارد.

آزادسازی حافظه پس از استفاده

در PHP، حافظه‌ی متغیرهایی که از scope خارج می‌شوند، آزاد می‌شود. ولی در حلقه‌های بزرگ، اگر reference نگه دارید، این آزادسازی ممکن نیست:

foreach ($large_data as $item) {
    $processed = process($item);
    save($processed);
    // $processed و $item در iteration بعدی جایگزین می‌شوند
    // به شرطی که reference نگه نداشته باشید
}
unset($large_data); // آزادسازی صریح
unset($processed);

حذف صریح با unset()

در حلقه‌های سنگین، استفاده از unset() برای متغیرهای سنگین، تفاوت جدی دارد. مخصوصاً اگر آن متغیر در iteration بعدی بازنویسی نشود، بدون unset همچنان در حافظه می‌ماند.

Reference، Copy-on-Write و رفتار پنهان PHP

PHP یک مکانیزم به نام Copy-on-Write دارد که درک آن برای مدیریت حافظه حیاتی است. وقتی یک آرایه را به متغیر دیگری assign می‌کنید، PHP آرایه را کپی نمی‌کند، بلکه هر دو متغیر به یک داده‌ی مشترک اشاره می‌کنند. تا زمانی که یکی از آن‌ها را تغییر ندهید، حافظه‌ی اضافی مصرف نمی‌شود.

مثال:

$a = range(1, 1000000); // یک میلیون عنصر
$b = $a; // هنوز همان داده، حافظه‌ی اضافی صفر
$b[0] = 999; // حالا $b کپی می‌شود، حافظه دو برابر

این رفتار، در کدهای اشتباه می‌تواند باعث مصرف غیرمنتظره‌ی حافظه شود. اگر روی $b عمل‌های نوشتن انجام دهید بدون اینکه بدانید، حافظه دو برابر می‌شود.

در مقابل، استفاده از reference با & باعث می‌شود دو متغیر به یک داده اشاره کنند، ولی در این حالت، تغییر در یکی، دیگری را هم تغییر می‌دهد. این دو رویکرد، تریدآف بین مصرف حافظه و پیچیدگی کد هستند.

یک نکته‌ی ظریف: در PHP 7 و 8، ساختار داده‌ی داخلی آرایه‌ها بهینه‌تر شده، ولی مکانیزم Copy-on-Write همچنان برجاست. اگر با کدهای PHP قدیمی کار می‌کنید که مصرف حافظه‌ی بالا دارند، احتمالاً الگوهای Copy-on-Write را در نظر نگرفته‌اند.

روش تشخیص اصولی نشتی حافظه

تشخیص مصرف بالای حافظه، ساده‌تر از تشخیص نشتی (leak) حافظه است. اولی با یک نگاه به کد مشخص می‌شود؛ دومی نیاز به ابزار دارد. در تجربه‌ی من، ترکیب چند روش بهترین نتیجه را می‌دهد.

روش اول: memory_get_usage

تابع ساده ولی موثر برای لاگ کردن مصرف حافظه در نقاط مختلف کد:

error_log("Memory at start: " . memory_get_usage());
// ... عملیات
error_log("Memory after: " . memory_get_usage());

// حالت واقعی - حافظه‌ی مصرفی توسط PHP، نه سیستم
error_log("Real memory: " . memory_get_usage(true));

// اوج مصرف حافظه از ابتدای اسکریپت
error_log("Peak: " . memory_get_peak_usage());

این تابع، در تشخیص سریع مفید است. اگر بین دو نقطه، افزایش چشمگیر دیده شود، همان بخش مشکوک است.

روش دوم: لاگ‌گیری در حلقه‌های بزرگ

در حلقه‌های بزرگ، در بازه‌های منظم، مصرف حافظه را لاگ کنید:

foreach ($items as $i => $item) {
    if ($i % 1000 === 0) {
        error_log("Item $i, Memory: " . memory_get_usage());
    }
    // پردازش
}

اگر مصرف حافظه در طول حلقه به‌طور مداوم افزایش می‌یابد، یک نشتی دارید. اگر ثابت می‌ماند، مصرف شما توجیه‌پذیر است.

روش سوم: Xdebug

Xdebug یک حالت Profiler دارد که مصرف حافظه‌ی هر تابع را نشان می‌دهد. اجرای این profiler در محیط staging با داده‌ی واقعی، دقیق‌ترین تصویر را می‌دهد. این ابزار، در پروژه‌های پیچیده، تفاوت جدی ایجاد می‌کند.

روش چهارم: Blackfire یا Tideways

ابزارهای تجاری مثل Blackfire و Tideways، پروفایل حرفه‌ای PHP ارائه می‌دهند. در پروژه‌های سازمانی، این ابزارها بخشی از CI/CD هستند.

روش پنجم: gc_collect_cycles

PHP یک Garbage Collector دارد که reference cycleها را آزاد می‌کند. اگر شک دارید که یک حلقه‌ی طولانی نشتی دارد، می‌توانید دستی gc را فراخوانی کنید:

gc_collect_cycles();

اگر این فراخوانی، مصرف حافظه را کم کرد، یعنی reference cycle داشتید. این تکنیک، در کدهایی که با objectهای تودرتو کار می‌کنند، مفید است.

راه‌حل Streaming و Batch

راه‌حل اصلی برای خطای Memory limit، تغییر در نحوه‌ی پردازش داده است. دو الگوی اصلی وجود دارد: Streaming و Batch.

الگوی Streaming

Streaming یعنی داده را به‌صورت قطعه‌ای بخوانید و پردازش کنید، بدون اینکه همه در حافظه باشد:

$handle = fopen("large.csv", "r");
while (($row = fgetcsv($handle)) !== false) {
    save_to_db($row);
}
fclose($handle);

حافظه‌ی مصرفی این الگو، ثابت است و به اندازه‌ی فایل بستگی ندارد. حتی می‌توانید یک فایل 10 گیگابایتی را با 50 مگابایت حافظه پردازش کنید.

الگوی Batch

در Batch، داده را در دسته‌های مشخص پردازش می‌کنید:

$batch_size = 1000;
$offset = 0;
while (true) {
    $rows = $db->query("SELECT * FROM users LIMIT $offset, $batch_size");
    if (empty($rows)) {
        break;
    }
    foreach ($rows as $row) {
        process($row);
    }
    $offset += $batch_size;
}

این الگو، در پردازش جداول بزرگ، استاندارد است. تعداد batch را می‌توانید طوری تنظیم کنید که حافظه‌ی مصرفی در محدوده‌ی ایمن باشد.

ترکیب Streaming و Batch

در پروژه‌های بزرگ، ترکیب این دو الگو موثرترین راه است:

$offset = 0;
$batch_size = 500;
while (true) {
    $rows = fetch_batch($offset, $batch_size);
    if (empty($rows)) {
        break;
    }
    foreach ($rows as $row) {
        process_row($row);
        unset($row); // آزادسازی صریح
    }
    unset($rows); // آزادسازی صریح
    gc_collect_cycles(); // جمع‌آوری reference cycleها
    $offset += $batch_size;
}

این الگو، در پروژه‌هایم تبدیل به یک template شده که تقریباً در تمام اسکریپت‌های پردازش داده استفاده می‌کنم. حافظه‌ی مصرفی آن، در تمام طول اجرا ثابت است.

Generatorها و صرفه‌جویی چشمگیر در حافظه

Generators یکی از قدرتمندترین ویژگی‌های PHP است که کمتر از حد انتظار استفاده می‌شود. Generator، تابعی است که به‌جای بازگرداندن آرایه، مقادیر را یکی‌یکی تولید می‌کند:

function read_lines($file) {
    $handle = fopen($file, "r");
    while (($line = fgets($handle)) !== false) {
        yield $line;
    }
    fclose($handle);
}

foreach (read_lines("huge.txt") as $line) {
    process($line);
}

در این الگو، حتی یک میلیون خط هم در یک لحظه در حافظه بارگذاری نمی‌شود. هر خط در زمان پردازش، از stream خوانده می‌شود. تفاوت حافظه، در تجربه‌ی من، گاهی از چند صد مگابایت به چند کیلوبایت کاهش می‌یابد.

Generators در PHP 5.5 معرفی شدند و از آن زمان، یکی از بهترین ابزارها برای مدیریت حافظه هستند. اگر با کدهایی کار می‌کنید که آرایه‌های بزرگ برمی‌گردانند، تبدیل آن‌ها به Generator تفاوت جدی ایجاد می‌کند.

Cache، Trade-off بین سرعت و حافظه

Cache می‌تواند به کاهش مصرف حافظه کمک کند - چون داده را یک‌بار محاسبه می‌کند و در دفعات بعد استفاده می‌کند - ولی Cache در حافظه (مثل APCu) خودش حافظه مصرف می‌کند. مدیریت این تریدآف، یک تصمیم طراحی است.

استراتژی من در پروژه‌های مختلف:

  • برای داده‌های کوچک و پرمصرف: Cache در APCu یا Redis، چون سرعت دسترسی بالا و حافظه‌ی مصرفی محدود است.
  • برای داده‌های بزرگ و کم‌مصرف: Cache در دیسک، چون حافظه‌ی سرور محدودتر از دیسک است.
  • برای داده‌های موقت: Cache در حافظه با TTL کوتاه، تا خودبه‌خود آزاد شود.

یک نکته‌ی ظریف: در پروژه‌های وردپرسی، Cache اغلب در دیتابیس ذخیره می‌شود، نه در حافظه. این رویکرد، مصرف حافظه‌ی PHP را کم می‌کند، ولی فشار روی دیتابیس را افزایش می‌دهد. انتخاب بین این دو، بستگی به bottleneck اصلی سیستم دارد.

Memory limit در محیط production

در محیط production، این خطا ابعاد متفاوتی پیدا می‌کند:

پیامدهای پنهان

وقتی این خطا در production رخ می‌دهد:

  1. تراکنش نیمه‌کاره: اگر اسکریپت در میانه‌ی یک عملیات بزرگ متوقف شود، داده‌ی ناقص باقی می‌ماند.
  2. صف انباشته: اگر چندین درخواست همزمان خطا بدهند، صف درخواست‌ها انباشته می‌شود.
  3. پاسخ نادرست به کاربر: خطای 500 یا صفحه‌ی سفید، تجربه‌ی کاربری را خراب می‌کند.
  4. افزایش هزینه: در سرویس‌های ابری، هر بار اجرای اسکریپتی که با خطا متوقف می‌شود، هزینه‌ای دارد که بازدهی نداشته.

راهبرد مدیریت

در production، سه کار انجام می‌دهم:

یک: لاگ‌گیری ساختارمند از هر خطای حافظه، با context کامل. peak memory usage، URL، user id، و پارامترهای ورودی.

دو: Alerting روی نرخ این خطا. اگر نرخ بالای مشخصی رفت، بررسی فوری.

سه: بازنگری ماهانه‌ی مصرف حافظه‌ی کل اپلیکیشن. اگر مصرف در طول زمان به‌آرامی افزایش می‌یابد، یعنی نشتی دارید که باید قبل از بحران پیدا شود.

نکته‌ی امنیتی: بعضی از حملات، با ارسال درخواست‌های بزرگ، سعی می‌کنند حافظه‌ی سرور را پر کنند. مرور مباحث امنیت در PHP به تشخیص این الگوها کمک می‌کند.

اشتباهات رایج در برخورد با این خطا

در طول سال‌ها، الگوهای تکراری از اشتباهات دیده‌ام که هر کدام می‌تواند پروژه را به چالش بکشد:

اشتباه اول: افزایش کورکورانه‌ی محدودیت

شایع‌ترین اشتباه. افزایش از 128M به 512M یا 1G، خطا را پنهان می‌کند ولی ریشه را حل نمی‌کند. راه‌حل: تشخیص و بازنویسی.

اشتباه دوم: استفاده از unset اشتباه

بعضی توسعه‌دهنده‌ها فکر می‌کنند unset($var) حافظه را آزاد می‌کند. این تابع، reference را حذف می‌کند، ولی داده فقط زمانی آزاد می‌شود که هیچ reference دیگری نداشته باشد. در PHP 7+ با reference counting، آزادسازی معمولاً خودکار است.

اشتباه سوم: نادیده گرفتن memory_get_usage(true)

تفاوت بین memory_get_usage() و memory_get_usage(true) مهم است. اولی حافظه‌ی مصرفی متغیرها را می‌دهد؛ دومی حافظه‌ی واقعی که از سیستم گرفته شده را نشان می‌دهد. اگر PHP حافظه را آزاد کرده ولی به سیستم پس نداده، عدد دوم بزرگ‌تر می‌ماند. برای تشخیص دقیق، همیشه هر دو را چک کنید.

اشتباه چهارم: بی‌توجهی به limit ستون‌های دیتابیس

اگر memory_limit روی 256M باشد و شما کوئری با LIMIT 1000000 بزنید، دیتابیس خودش ممکن است با محدودیت داخلی مواجه شود، نه PHP. این تفکیک، در تشخیص مهم است. مبانی محدودیت‌های دیتابیس در یادگیری MySQL از صفر آمده است.

اشتباه پنجم: تست نکردن با داده‌ی واقعی

اگر در development با 1000 ردیف تست کنید، هیچ‌وقت به محدودیت حافظه نمی‌رسید. در production با 10 میلیون ردیف، ممکن است بله. راه‌حل: staging با داده‌ی مشابه production.

اشتباه ششم: استفاده از فایل‌های بزرگ بدون Streaming

بارگذاری تصویر یا PDF بزرگ با file_get_contents، سریع‌ترین راه به خطای حافظه است. راه‌حل: استفاده از خواندن قطعه‌ای با fopen و fread.

اشتباه هفتم: نگه‌داشتن داده‌ی اضافی در متغیرهای سراسری

متغیرهای سراسری در طول اجرای اسکریپت باقی می‌مانند. اگر داده‌ی بزرگ را در global ذخیره کنید، تا پایان اسکریپت در حافظه می‌ماند. راه‌حل: استفاده از متغیرهای محلی و آزادسازی صریح.

در مدیریت حافظه PHP، سه کلمه کلیدی است: Stream، Batch، و Unset. اگر این سه کلمه در دایره‌المعارف کد شما هستند، خطای Memory limit خیلی کم پیش می‌آید.

پرسش‌های پرتکرار درباره خطای Memory limit در PHP

این پرسش‌ها از تجربه‌ی عملی و جلسات مشاوره جمع‌آوری شده‌اند. پاسخ هر کدام بر اساس سناریوهای واقعی است.

memory_limit باید چه مقداری باشد؟

مقدار مناسب، بستگی به نوع پروژه دارد. برای سایت‌های شخصی و وبلاگ، 128M معمولاً کافی است. برای سایت‌های شرکتی و فروشگاهی کوچک، 256M. برای فروشگاه‌های بزرگ و پروژه‌های پردازش داده، 512M تا 1G. مقدار استاندارد امروز 256M است، ولی مهم‌تر از مقدار، مصرف واقعی اپلیکیشن است. اگر مصرف متوسط کمتر از نصف محدودیت است، محدودیت مناسب است.

آیا روی همه هاست‌ها می‌توانم memory_limit را افزایش دهم؟

روی هاست‌های اشتراکی، اغلب محدودیت سختگیرانه وجود دارد و افزایش بالاتر از مقدار مشخصی مجاز نیست. روی سرورهای اختصاصی یا VPS، می‌توانید به هر مقداری تنظیم کنید. روی سرویس‌های ابری مدیریت‌شده، بستگی به پلن دارد.

تفاوت memory_limit و max_memory_limit چیست؟

در PHP فقط memory_limit وجود دارد، ولی در محیط‌های مدیریت‌شده مثل هاست‌های اشتراکی، ممکن است مقدار بالاتری وجود داشته باشد که به‌عنوان سقف مجاز تلقی می‌شود. اگر سعی کنید بالاتر از آن بروید، مقدار شما نادیده گرفته می‌شود. با phpinfo() مقدار واقعی را بررسی کنید.

چرا در CLI محدودیت حافظه متفاوت است؟

در حالت CLI، memory_limit معمولاً -1 است، یعنی بدون محدودیت. این طراحی برای اسکریپت‌های پردازش داده و cron jobها منطقی است. ولی این باعث می‌شود که در CLI، خطای Memory limit دیده نشود، در حالی که در وب، خطا بدهد. راه‌حل: در staging، هر دو حالت را تست کنید.

آیا این خطا با خطای Maximum execution time ارتباط دارد؟

نه مستقیماً، ولی غیرمستقیم بله. اگر یک اسکریپت هم حافظه‌ی زیاد مصرف می‌کند و هم زمان زیاد می‌برد، ممکن است هر دو خطا رخ دهند. راه‌حل در هر دو حالت، بازنویسی با الگوهای Streaming و Batch است. جزئیات در خطای Maximum execution time آمده است.

چگونه در وردپرس memory_limit را افزایش دهم؟

در فایل wp-config.php خط زیر را اضافه کنید:

define("WP_MEMORY_LIMIT", "256M");
define("WP_MAX_MEMORY_LIMIT", "512M");

خط اول برای عملیات عادی، خط دوم برای عملیات admin (مثل آپلود تصویر بزرگ). اگر این تنظیمات اعمال نشد، احتمالاً هاست شما ini_set را محدود کرده و باید با پشتیبانی صحبت کنید.

آیا با APC یا APCu می‌توانم مشکل را حل کنم؟

APC و APCu، حافظه‌ی cache خودشان را دارند و مصرف حافظه‌ی PHP را کم می‌کنند، ولی خودشان هم حافظه مصرف می‌کنند. اگر bottleneck اصلی حافظه‌ی PHP است، APC می‌تواند کمک کند، ولی این راه‌حل برای همه‌ی سناریوها مناسب نیست. برای پردازش داده‌های بزرگ، راه‌حل اصلی، Streaming و Batch است.

چرا file_get_contents روی فایل 100 مگابایتی خطا می‌دهد؟

چون file_get_contents کل فایل را در حافظه بار می‌کند و ممکن است PHP چند نسخه از داده را نگه دارد. برای فایل‌های بزرگ، از fopen و fread یا از Generator استفاده کنید. الگوی کامل در رفع خطای Failed to open stream آمده است.

آیا Generators همیشه بهتر از آرایه هستند؟

Generators در مصرف حافظه بهتر هستند ولی معایبی هم دارند: یک‌بار مصرف هستند (بعد از یک بار پیمایش، دیگر قابل استفاده نیستند)، نمی‌توانید روی آن‌ها index بزنید، و نمی‌توانید count کنید بدون پیمایش کامل. برای سناریوهایی که نیاز به دسترسی تصادفی یا چندین پیمایش دارید، آرایه بهتر است.

چطور بفهمم کدام تابع حافظه‌ی زیاد مصرف می‌کند؟

با Xdebug Profiler یا Blackfire، می‌توانید مصرف حافظه‌ی هر تابع را دقیق ببینید. برای تشخیص سریع، از memory_get_usage قبل و بعد از هر تابع مشکوک استفاده کنید. برای یک analysis دقیق، ترکیب چند ابزار توصیه می‌شود.

آیا این خطا روی performance سایت اثر دارد؟

خود خطا، فقط در لحظه‌ی وقوع. ولی مصرف بالای حافظه در طول اجرا، تأثیر دارد: page load کندتر، سرور پرفشارتر، و در نهایت تجربه‌ی کاربری پایین‌تر. حتی اگر خطا رخ ندهد، مصرف بالا یک سیگنال منفی است.

تفاوت Peak Memory و Current Memory چیست؟

memory_get_usage() مصرف لحظه‌ای را می‌دهد. memory_get_peak_usage() اوج مصرف از ابتدای اسکریپت را نشان می‌دهد. برای تشخیص، peak کاربردی‌تر است، چون ممکن است در نقطه‌ای اوج گرفته باشد و دوباره کم شده باشد. برای tuning محدودیت، peak را معیار بگیرید.

آیا حافظه‌ی PHP روی سرور به همه‌ی اسکریپت‌ها یکسان است؟

نه. هر پروسه‌ی PHP مستقل است. اگر سایت شما روی PHP-FPM با 10 worker اجرا شود، ممکن است تا 10 برابر memory_limit حافظه‌ی سرور مصرف کند (در اوج). این نکته، در محاسبه‌ی ظرفیت سرور حیاتی است.

چگونه در وردپرس Memory limit را پایش کنم؟

افزونه‌هایی مثل Query Monitor یا WP Memory Usage، مصرف حافظه‌ی وردپرس را نشان می‌دهند. اگر مصرف به‌طور مداوم نزدیک محدودیت است، یعنی یک افزونه یا قالب مشکل‌دار دارید. تشخیص دقیق منبع، با ابزارهای Profiler ممکن است.

آیا می‌توانم memory_limit را در runtime بالاتر از مقدار اولیه ببرم؟

در بیشتر موارد بله، ولی محدودیت‌هایی وجود دارد. اگر مقدار در php.ini با محدودیت سخت تنظیم شده باشد، ini_set ممکن است نادیده گرفته شود. در هاست‌های اشتراکی، این محدودیت شایع است.

آیا استفاده از Reference & باعث کاهش حافظه می‌شود؟

در بعضی موارد بله، در بعضی موارد نه. اگر دو متغیر به یک داده‌ی بزرگ اشاره کنند، reference حافظه‌ی اضافی مصرف نمی‌کند. ولی استفاده‌ی نادرست از reference می‌تواند باگ‌های ظریف ایجاد کند (چون تغییر در یکی، دیگری را هم تغییر می‌دهد). توصیه: فقط در مواردی که کاملاً مطمئنید، از reference استفاده کنید.

آیا آپگرید PHP نسخه، حافظه‌ی مصرفی را کمتر می‌کند؟

در بعضی موارد بله. PHP 7 و 8 ساختار داخلی بهینه‌تری دارند و آرایه‌ها حافظه‌ی کمتری مصرف می‌کنند. اگر پروژه‌ای با PHP 5.6 روی حافظه‌ی زیاد کار می‌کند، مهاجرت به PHP 8.x می‌تواند 30 تا 50 درصد مصرف حافظه را کاهش دهد.

چرا بعضی از سایت‌ها با memory_limit 512M هم خطا می‌دهند؟

چون کد آن سایت، مسئله‌ی حافظه را در معماری خودش حل نکرده است. محدودیت را بالا می‌برند، ولی الگوی مصرف همچنان بالا است. راه‌حل واقعی، بازنویسی معماری است، نه افزایش پی‌درپی محدودیت.

آیا کش کردن دیتابیس می‌تواند مصرف حافظه را کم کند؟

بله، اگر کش در حافظه‌ی خارج از PHP باشد. مثلاً Redis یا Memcached، داده را در پروسه‌ی مستقل ذخیره می‌کنند و PHP فقط با آن تعامل دارد. این رویکرد، مصرف حافظه‌ی PHP را کم می‌کند ولی فشار روی سرورهای جداگانه را افزایش می‌دهد.

آیا حجم کوئری دیتابیس روی memory_limit اثر دارد؟

بله. وقتی با fetchAll() یا معادل آن در PDO/MySQLi استفاده می‌کنید، تمام نتایج در حافظه‌ی PHP ذخیره می‌شود. برای نتایج بزرگ، از fetch() در حلقه یا از streaming استفاده کنید. مبانی کار با دیتابیس در اتصال PHP به MySQL و الگوهای PDO در آموزش PDO در PHP آمده است.

چگونه بعد از خطای Memory limit، دیباگ را شروع کنم؟

اول پیام خطا را کامل بخوانید - مقدار مجاز و مقدار تلاش‌شده را ثبت کنید. دوم، خط دقیق را در کد پیدا کنید و سه خط قبل و بعد را ببینید. سوم، در همان بخش، از memory_get_usage برای لاگ کردن استفاده کنید. چهارم، الگوی Streaming یا Batch را جایگزین کنید. این چهار گام، در 90 درصد موارد به راه‌حل می‌رسد.

درس‌هایی از سال‌ها کار با محدودیت حافظه در PHP

اگر بخواهم چکیده‌ی این سال‌ها را در چند جمله بگویم، سه اصل عملی دارم که در تمام پروژه‌هایم رعایت می‌کنم:

یک: حافظه، سرمایه‌ای محدود است. مثل یک میز کار که ظرفیت مشخصی دارد. اگر داده‌ی بزرگ را روی میز ریختید، جای کار کردن ندارید. راه‌حل، استفاده از کشوهای مختلف - یا در زبان استعاری، Streaming و Batch.

دو: افزایش محدودیت، مدیریت نیست. هر بار که memory_limit را بالا می‌برید، هزینه‌ی واقعی را به آینده منتقل می‌کنید. مدیریت درست، مصرف بهینه است، نه منابع نامحدود.

سه: کد حرفه‌ای، با یک میلیون ردیف هم کار می‌کند. اگر کد شما با 1000 ردیف کار می‌کند ولی با یک میلیون خطا می‌دهد، کد شما حرفه‌ای نیست. کد حرفه‌ای، در هر اندازه‌ای مصرف حافظه‌اش پیش‌بینی‌پذیر است.

در کنار این سه اصل، یک هشدار عملی هم دارم: خطای Memory limit در محیط development خیلی وقت‌ها دیده نمی‌شود، چون داده‌ی تست کم است. این باعث می‌شود تیم‌ها فکر کنند مسئله‌ی حافظه ندارند، در حالی که در production با داده‌ی واقعی، خطا ظاهر می‌شود. راه‌حل: staging با داده‌ی مشابه production، و پایش مداوم مصرف حافظه.

خطای Memory limit در PHP، در نگاه اول یک محدودیت تحمیلی به نظر می‌رسد. ولی در باطن، یک فرصت آموزشی است. این خطا می‌گوید کد شما از بودجه‌ی حافظه‌اش تجاوز کرده. اگر این پیام را جدی بگیرید و به‌جای افزایش بودجه، الگوی مصرف را اصلاح کنید، پروژه‌ی شما به سطحی از پایداری می‌رسد که در بلندمدت تفاوت جدی ایجاد می‌کند.

هدف این مقاله، تمام‌کردن همه‌ی سناریوهای ممکن نبود. هدف، دادن یک چارچوب ذهنی برای تشخیص، بازنویسی و پایش خطای حافظه بود. وقتی این چارچوب را درونی کنید، برخورد با این خطا از یک واکنش اضطراری به یک فرآیند منظم تبدیل می‌شود.

اگر خطای Memory limit در پروژه‌ی شما به شکلی ظاهر شده که با الگوهای این مقاله حل نشده، برای من جالب است بدانم کدام سناریو بود. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید؛ به‌ویژه اگر راه‌حلی پیدا کرده‌اید که هنوز در این مقاله نیست. 💾