PHPで Fatal error: Allowed memory size of XXXXX bytes exhausted というエラーが発生して困っていませんか?
このエラーは、PHPスクリプトが使用できるメモリの上限を超えたときに発生する致命的エラー(Fatal error)です。スクリプトはその場で強制終了され、それ以降の処理は一切実行されません。
この記事では、Allowed memory size exhausted エラーの原因となる全パターンと状況別の具体的な解決方法を、実際のコード例付きで徹底解説します。
この記事でわかること
- エラーメッセージの読み方と意味
- メモリ不足を引き起こす主な原因4パターン
- memory_limit の正しい変更方法(php.ini / .htaccess / ini_set / レンタルサーバー)
- コード最適化によるメモリ削減テクニック(ジェネレータ・ストリーム・チャンク処理)
- WordPress でのメモリ不足対策
- メモリ使用量の調査・プロファイリング方法
- 実務で役立つパターン集(CSV・画像・API・PDF・ログ)
1. 「Fatal error: Allowed memory size」エラーとは?
1-1. エラーメッセージの読み方
まず、実際のエラーメッセージを確認しましょう。
エラーメッセージ例
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 65536 bytes) in /var/www/html/example.php on line 42
このエラーメッセージには、問題を特定するための重要な情報が含まれています。
| 部分 |
意味 |
上記の例 |
Fatal error |
致命的エラー(スクリプト停止) |
– |
Allowed memory size of X bytes |
設定されたメモリ上限 |
134217728 bytes = 128MB |
exhausted |
メモリを使い果たした |
– |
tried to allocate Y bytes |
追加で確保しようとしたサイズ |
65536 bytes = 64KB |
in /path/file.php on line N |
エラーが発生したファイルと行番号 |
example.php の 42行目 |
ポイント:バイト数を人間が読みやすい単位に変換する早見表: 134217728 = 128MB、268435456 = 256MB、536870912 = 512MB、1073741824 = 1GB
1-2. memory_limit とは
PHPには memory_limit という設定値があり、1つのスクリプトが使用できるメモリの最大量を制御しています。
PHP – memory_limit の確認
<?php
// 現在のmemory_limitを確認
echo '現在のmemory_limit: ' . ini_get('memory_limit');
// 出力例: 現在のmemory_limit: 128M
memory_limit のデフォルト値はPHPのバージョンや環境によって異なります。
| 環境 |
デフォルト値 |
備考 |
| PHP 5.x |
128M |
– |
| PHP 7.x |
128M |
– |
| PHP 8.x |
128M |
– |
| 共用レンタルサーバー |
32M〜512M |
サーバーにより大きく異なる |
| CLI(コマンドライン) |
-1(無制限) |
php.ini で別途設定可能 |
注意:memory_limit = -1 に設定するとメモリ制限が無制限になりますが、本番環境では推奨されません。サーバー全体のメモリを使い切り、他のプロセスに影響を与える可能性があります。
1-3. メモリ制限の仕組み
PHPのメモリ管理の仕組みを理解しておきましょう。
メモリ管理の流れ
1. PHPスクリプト実行開始
↓
2. 変数の宣言、関数の実行ごとにメモリを確保
↓
3. 使用メモリが memory_limit に達した
↓
4. Fatal error: Allowed memory size exhausted → スクリプト停止
PHPはスクリプト実行中、変数や配列、オブジェクトなどを作成するたびにメモリを消費します。通常はスクリプト終了時にすべてのメモリが解放されますが、スクリプト実行中に上限に達した場合は Fatal error となります。
PHP – メモリ消費の可視化
<?php
echo '開始時: ' . memory_get_usage() . " bytes
";
// 1万要素の配列を作成
$array = range(1, 10000);
echo '配列作成後: ' . memory_get_usage() . " bytes
";
// 大きな文字列を作成
$string = str_repeat('A', 1000000);
echo '文字列作成後: ' . memory_get_usage() . " bytes
";
// メモリ解放
unset($array, $string);
echo '解放後: ' . memory_get_usage() . " bytes
";
実行結果
開始時: 393216 bytes
配列作成後: 921600 bytes
文字列作成後: 1921600 bytes
解放後: 393216 bytes
1-4. PHPのデータ型ごとのメモリ消費量
PHPの各データ型がどの程度のメモリを消費するか知っておくと、メモリ見積もりに役立ちます。
| データ型 |
メモリ消費量(PHP 8.x 64bit) |
備考 |
| integer |
16 bytes(zval) |
– |
| float |
16 bytes(zval) |
– |
| boolean |
16 bytes(zval) |
– |
| string |
文字列長 + 約40 bytes |
UTF-8の場合は文字数×バイト数 |
| 配列の各要素 |
約72 bytes / 要素 |
ハッシュテーブルのオーバーヘッド含む |
| オブジェクト |
約56 bytes + プロパティ分 |
– |
ポイント:例えば100万行のデータを配列に格納すると、1,000,000 × 72 bytes = 約69MBのメモリが必要です。各要素にさらにデータが含まれると、あっという間にメモリ上限に達します。
2. 原因①:大量データの読み込み
メモリ不足の最も一般的な原因は、大量のデータを一度にメモリに読み込むことです。
2-1. file_get_contents() で巨大ファイルを一括読み込み
file_get_contents() はファイルの内容をすべてメモリに読み込む関数です。数MB程度のファイルなら問題ありませんが、数百MB以上のファイルを読み込むと簡単にメモリ上限に達します。
PHP – メモリ不足になるコード(悪い例)
<?php
// ❌ 500MBのログファイルを一括読み込み → メモリ不足
$content = file_get_contents('/var/log/app/huge-logfile.log');
// ❌ 巨大なJSONファイルを一括読み込み
$json = file_get_contents('large-data.json');
$data = json_decode($json, true);
// ❌ file() も全行を配列に読み込む
$lines = file('/var/log/app/huge-logfile.log');
PHP – ストリーム処理で解決(良い例)
<?php
// ✅ fopen + fgets で1行ずつ処理
$handle = fopen('/var/log/app/huge-logfile.log', 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
// 1行ずつ処理(メモリ消費は1行分のみ)
processLine($line);
}
fclose($handle);
}
// ✅ SplFileObject を使う方法
$file = new SplFileObject('/var/log/app/huge-logfile.log');
while (!$file->eof()) {
$line = $file->fgets();
processLine($line);
}
2-2. 大量のDBレコードを一括取得
データベースから大量のレコードを一度に取得すると、全レコードがメモリに展開されます。
PHP – メモリ不足になるDBクエリ(悪い例)
<?php
// ❌ 100万レコードを一括取得 → メモリ不足
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$stmt = $pdo->query('SELECT * FROM huge_table');
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
processRow($row);
}
PHP – 1行ずつフェッチで解決(良い例)
<?php
// ✅ fetchAll() ではなく fetch() で1行ずつ処理
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
// バッファリングを無効化して省メモリ
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $pdo->query('SELECT * FROM huge_table');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
processRow($row);
// メモリ消費は常に1行分のみ
}
2-3. CSVファイルの全行読み込み
CSVファイルの処理でも同様の問題が発生します。
PHP – CSV処理のメモリ問題と解決策
<?php
// ❌ CSVを全行読み込んで配列に格納
$rows = [];
$csv = file('huge-data.csv');
foreach ($csv as $line) {
$rows[] = str_getcsv($line);
}
// ✅ fgetcsv で1行ずつ処理
$handle = fopen('huge-data.csv', 'r');
$header = fgetcsv($handle); // ヘッダー行
while (($row = fgetcsv($handle)) !== false) {
// 1行ずつ処理
$data = array_combine($header, $row);
insertToDatabase($data);
}
fclose($handle);
| 方法 |
メモリ使用量(100万行CSV) |
処理速度 |
file() + str_getcsv() |
約500MB以上 |
遅い(一括読み込み) |
fgetcsv() |
約1MB(一定) |
速い |
SplFileObject |
約1MB(一定) |
速い |
3. 原因②:無限ループ・再帰
プログラムのバグにより、ループが終了しない、または再帰呼び出しが深くなりすぎる場合もメモリ不足の原因になります。
3-1. 終了条件のない while / for / foreach
PHP – 無限ループでメモリ不足(悪い例)
<?php
// ❌ 例1: カウンタを増加させ忘れ → 無限ループ
$i = 0;
$results = [];
while ($i < 100) {
$results[] = fetchData($i);
// $i++ を忘れている! → $results が無限に増加
}
// ❌ 例2: 条件が常にtrueになるバグ
$page = 1;
$allData = [];
while ($page > 0) { // > 0 は常にtrue($pageは増え続ける)
$data = fetchPage($page);
$allData = array_merge($allData, $data);
$page++;
}
PHP – 正しいループの書き方(良い例)
<?php
// ✅ 例1: カウンタを正しく増加
$results = [];
for ($i = 0; $i < 100; $i++) {
$results[] = fetchData($i);
}
// ✅ 例2: API ページネーションの正しい実装
$page = 1;
$maxPages = 100; // 安全のための上限
$allData = [];
while ($page <= $maxPages) {
$data = fetchPage($page);
if (empty($data)) break; // データがなければ終了
$allData = array_merge($allData, $data);
$page++;
}
3-2. 再帰関数の深すぎるネスト
再帰関数は、適切な終了条件がないと無限に呼び出しが深くなり、スタックメモリを消費し続けます。
PHP – 再帰によるメモリ不足と対策
<?php
// ❌ 終了条件が不適切な再帰
function factorial($n) {
return $n * factorial($n - 1);
// $n が0以下になっても止まらない
}
// ✅ 正しい終了条件
function factorialSafe($n) {
if ($n <= 1) return 1;
return $n * factorialSafe($n - 1);
}
// ✅ 深さ制限付きツリー構築
function buildTree($parentId, $depth = 0, $maxDepth = 10) {
if ($depth >= $maxDepth) return [];
$children = getChildren($parentId);
$tree = [];
foreach ($children as $child) {
$child['children'] = buildTree($child['id'], $depth + 1, $maxDepth);
$tree[] = $child;
}
return $tree;
}
// ✅ 再帰をループに書き換え
function factorialIterative($n) {
$result = 1;
for ($i = 2; $i <= $n; $i++) {
$result *= $i;
}
return $result;
}
3-3. 循環参照によるメモリリーク
オブジェクト同士が互いを参照し合う「循環参照」は、ガベージコレクタが正しく処理できない場合にメモリリークを引き起こします。
PHP – 循環参照の対策
<?php
// ❌ 循環参照の発生
class Node {
public $children = [];
public $parent = null;
}
$parent = new Node();
$child = new Node();
$parent->children[] = $child;
$child->parent = $parent; // 循環参照!
// ✅ 対策1: WeakReference を使う(PHP 7.4+)
class SafeNode {
public array $children = [];
public ?WeakReference $parent = null;
public function setParent(SafeNode $parent): void {
$this->parent = WeakReference::create($parent);
}
}
// ✅ 対策2: 手動でガベージコレクションを実行
for ($i = 0; $i < 100000; $i++) {
// ... 循環参照を含む処理 ...
if ($i % 1000 === 0) {
gc_collect_cycles();
}
}
4. 原因③:画像処理
画像処理は非常にメモリを消費する操作です。特に高解像度の画像を扱う場合、元のファイルサイズよりもはるかに多くのメモリが必要になります。
4-1. GDライブラリで巨大画像を扱う
画像のメモリ使用量の計算式
画像のメモリ使用量 = 幅 × 高さ × チャンネル数(通常4: RGBA)× 係数(約1.7)
例: 4000×3000ピクセルの画像 = 4000 × 3000 × 4 × 1.7 ≒ 約78MB
JPEGのファイルサイズが2MBでも、メモリ上では78MBになります。
PHP – 処理前にメモリ必要量をチェック
<?php
// ✅ 処理前にメモリ必要量をチェック
function checkImageMemory($imagePath) {
$info = getimagesize($imagePath);
if (!$info) return false;
$width = $info[0];
$height = $info[1];
$channels = $info['channels'] ?? 4;
$bits = $info['bits'] ?? 8;
// 必要メモリ(1.7倍の安全マージン)
$required = $width * $height * $channels * ($bits / 8) * 1.7;
$limit = returnBytes(ini_get('memory_limit'));
$available = $limit - memory_get_usage(true);
return $required < $available;
}
function returnBytes($val) {
$val = trim($val);
$last = strtolower($val[strlen($val) - 1]);
$val = (int)$val;
switch ($last) {
case 'g': $val *= 1024;
case 'm': $val *= 1024;
case 'k': $val *= 1024;
}
return $val;
}
// 使用例
if (checkImageMemory('huge-photo.jpg')) {
$image = imagecreatefromjpeg('huge-photo.jpg');
$resized = imagescale($image, 800, 600);
imagejpeg($resized, 'resized.jpg');
imagedestroy($image);
imagedestroy($resized);
} else {
echo '画像が大きすぎます。メモリが不足しています。';
}
4-2. 画像サイズごとのメモリ消費量一覧
| 画像サイズ(px) |
JPEGファイルサイズ |
GDメモリ消費(概算) |
必要なmemory_limit |
| 640 × 480 |
約100KB |
約2MB |
32M で十分 |
| 1920 × 1080 |
約500KB |
約14MB |
64M で十分 |
| 4000 × 3000 |
約2MB |
約78MB |
128M が必要 |
| 6000 × 4000 |
約5MB |
約163MB |
256M が必要 |
| 8000 × 6000 |
約10MB |
約326MB |
512M が必要 |
注意:リサイズ処理では、元画像とリサイズ後の画像の両方がメモリに存在します。上記の約2倍のメモリが必要です。
4-3. ImageMagick / exec による省メモリな画像処理
PHP – ImageMagick / exec による画像処理
<?php
// ✅ Imagick拡張を使う
$imagick = new Imagick('huge-photo.jpg');
$imagick->thumbnailImage(800, 600, true);
$imagick->writeImage('resized.jpg');
$imagick->clear();
$imagick->destroy();
// ✅ exec でコマンドライン実行(PHPのメモリを使わない)
$input = escapeshellarg('huge-photo.jpg');
$output = escapeshellarg('resized.jpg');
exec("convert {$input} -resize 800x600 {$output}");
5. 原因④:配列の肥大化
ループ内で配列にデータを追加し続けたり、不要な変数を解放しないと、メモリ使用量が徐々に増加します。
5-1. ループ内での配列への追加し続け
PHP – 配列肥大化の問題と対策
<?php
// ❌ ループ内で配列に追加し続ける
$allResults = [];
for ($page = 1; $page <= 1000; $page++) {
$data = fetchPageData($page);
$allResults = array_merge($allResults, $data);
// $allResults がどんどん大きくなる
}
// ✅ 各ページを処理してすぐ出力/保存
for ($page = 1; $page <= 1000; $page++) {
$data = fetchPageData($page);
processAndSave($data);
// $data はループの次の反復で上書きされる
}
// ✅ array_merge の代わりに [] を使う(メモリ効率が良い)
$results = [];
foreach ($items as $item) {
// ❌ array_merge は毎回配列全体をコピー
// $results = array_merge($results, $item);
// ✅ 直接追加(コピーなし)
$results[] = $item;
}
5-2. 不要な変数の未解放
PHP – unset() でメモリを積極的に解放
<?php
// ❌ 使い終わった巨大データをそのまま保持
$rawData = file_get_contents('data.json'); // 50MB
$parsed = json_decode($rawData, true); // さらに50MB以上
// $rawData はもう不要だがメモリに残っている
$filtered = filterData($parsed); // さらにメモリ消費
// ✅ 不要になったらすぐ解放
$rawData = file_get_contents('data.json');
$parsed = json_decode($rawData, true);
unset($rawData); // 50MB 解放!
$filtered = filterData($parsed);
unset($parsed); // さらに解放
5-3. グローバル変数・静的変数の蓄積
PHP – グローバル/静的変数の問題
<?php
// ❌ グローバル変数にデータを蓄積
$GLOBALS['log'] = [];
function addLog($message) {
$GLOBALS['log'][] = date('Y-m-d H:i:s') . ' ' . $message;
// ログが際限なく増加
}
// ❌ 静的変数にキャッシュを蓄積
function getCachedData($key) {
static $cache = [];
if (!isset($cache[$key])) {
$cache[$key] = expensiveComputation($key);
}
return $cache[$key];
// キーが多いとキャッシュが膨大に
}
// ✅ キャッシュに上限を設ける
function getCachedDataSafe($key, $maxCacheSize = 1000) {
static $cache = [];
if (!isset($cache[$key])) {
if (count($cache) >= $maxCacheSize) {
array_shift($cache); // 古いキャッシュを削除
}
$cache[$key] = expensiveComputation($key);
}
return $cache[$key];
}
6. 解決方法①:memory_limit の変更
最も手軽な対処法は、memory_limit の値を増やすことです。ただし、これは根本的な解決ではなく応急処置であることを理解しておきましょう。
6-1. php.ini で変更
サーバーの php.ini を直接編集できる場合は、最も確実な方法です。
php.ini
; デフォルト値
; memory_limit = 128M
; 256MBに増やす
memory_limit = 256M
; 512MBに増やす
; memory_limit = 512M
ターミナル – php.ini の場所を確認
# php.ini のパスを確認
$ php --ini
Configuration File (php.ini) Path: /etc/php/8.2/cli
Loaded Configuration File: /etc/php/8.2/cli/php.ini
# または phpinfo() で確認
$ php -i | grep memory_limit
memory_limit => 128M => 128M
ポイント:php.ini を変更した後は、Webサーバー(Apache/Nginx)の再起動が必要です。sudo systemctl restart apache2 または sudo systemctl restart php8.2-fpm
6-2. .htaccess で変更(Apache)
共用サーバーで php.ini を編集できない場合は、.htaccess で設定できる場合があります。
.htaccess
# memory_limit を 256MB に設定
php_value memory_limit 256M
# 特定のディレクトリだけ増やす場合
# そのディレクトリの .htaccess に記述
注意:.htaccess での設定は Apache + mod_php の環境でのみ有効です。PHP-FPM を使用している場合は効きません。
6-3. ini_set() でスクリプト内から変更
特定のスクリプトだけメモリ上限を増やしたい場合は、ini_set() を使います。
PHP – ini_set() での変更
<?php
// スクリプトの先頭で設定
ini_set('memory_limit', '256M');
// 変更が反映されたか確認
echo ini_get('memory_limit'); // 256M
// 一時的に増やして元に戻す
$originalLimit = ini_get('memory_limit');
ini_set('memory_limit', '512M');
// 重い処理を実行
heavyProcess();
// 元に戻す
ini_set('memory_limit', $originalLimit);
注意:共用レンタルサーバーでは ini_set() による memory_limit の変更が禁止されている場合があります。その場合はサーバーの管理パネルから設定します。
6-4. 各レンタルサーバーでの設定方法
| レンタルサーバー |
変更方法 |
上限 |
| エックスサーバー |
サーバーパネル → php.ini設定 → memory_limit を変更 |
最大 1G(プランにより異なる) |
| ロリポップ |
ユーザー専用ページ → PHP設定 → php.ini設定 |
最大 512M |
| さくらのレンタルサーバ |
コントロールパネル → スクリプト設定 → php.ini設定 |
最大 512M |
| ConoHa WING |
コントロールパネル → サイト設定 → PHP設定 |
最大 1G |
| mixhost |
cPanel → PHP設定 → memory_limit |
最大 512M |
エックスサーバーでの設定手順
1. サーバーパネルにログイン
2. 「php.ini設定」をクリック
3. 対象ドメインを選択
4. 「php.ini設定変更」タブを開く
5. 「memory_limit」の値を変更(例: 256M → 512M)
6. 「確認画面へ進む」→「変更する」
6-5. memory_limit の推奨値
| 用途 |
推奨値 |
備考 |
| 一般的なWebサイト |
128M |
通常はこれで十分 |
| WordPress(通常) |
256M |
プラグインが多い場合 |
| WordPress(管理画面) |
512M |
WP_MAX_MEMORY_LIMIT で設定 |
| 画像処理(GD/Imagick) |
256M〜512M |
扱う画像サイズによる |
| CSV/Excel大量処理 |
512M〜1G |
ストリーム処理推奨 |
| バッチ処理・CLI |
512M〜1G |
処理内容による |
7. 解決方法②:コードの最適化
memory_limit を増やすだけでなく、コード自体を最適化してメモリ消費を抑えることが根本的な解決策です。
7-1. ジェネレータ(yield)で大量データ処理
ジェネレータは、データを1件ずつ生成して返すため、大量データでもメモリ消費が一定に保たれます。
PHP – ジェネレータの基本
<?php
// ❌ 配列で全データを返す → メモリを大量消費
function getAllRecords(): array {
$records = [];
$stmt = $pdo->query('SELECT * FROM users');
while ($row = $stmt->fetch()) {
$records[] = $row;
}
return $records; // 全レコードがメモリに
}
// ✅ ジェネレータで1件ずつ返す → メモリ一定
function getAllRecordsGenerator(): Generator {
$stmt = $pdo->query('SELECT * FROM users');
while ($row = $stmt->fetch()) {
yield $row; // 1件返して一時停止
}
}
// 使い方は同じ
foreach (getAllRecordsGenerator() as $record) {
processRecord($record);
}
PHP – ジェネレータでCSV処理
<?php
// ✅ CSVをジェネレータで読み込み
function readCsv($filePath): Generator {
$handle = fopen($filePath, 'r');
$header = fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
yield array_combine($header, $row);
}
fclose($handle);
}
// 100万行のCSVでもメモリ消費は一定
foreach (readCsv('million-rows.csv') as $row) {
insertToDatabase($row);
}
// ✅ ジェネレータの連鎖(パイプライン)
function filterActive(Generator $rows): Generator {
foreach ($rows as $row) {
if ($row['status'] === 'active') {
yield $row;
}
}
}
function transformRow(Generator $rows): Generator {
foreach ($rows as $row) {
$row['name'] = mb_strtoupper($row['name']);
yield $row;
}
}
// パイプラインで連結
$csv = readCsv('users.csv');
$active = filterActive($csv);
$transformed = transformRow($active);
foreach ($transformed as $row) {
saveToDatabase($row);
}
| 処理方式 |
100万件処理時のメモリ |
特徴 |
| 配列で全件取得 |
数百MB〜数GB |
データ量に比例して増加 |
| ジェネレータ |
約1〜2MB(一定) |
データ量に依存しない |
7-2. ストリーム処理(fgetcsv, SplFileObject)
PHP – SplFileObject でCSV処理
<?php
// ✅ SplFileObject でオブジェクト指向的にCSV処理
$file = new SplFileObject('huge-data.csv');
$file->setFlags(
SplFileObject::READ_CSV |
SplFileObject::SKIP_EMPTY |
SplFileObject::READ_AHEAD
);
$header = $file->current();
$file->next();
foreach ($file as $row) {
if (count($row) === count($header)) {
$data = array_combine($header, $row);
processRow($data);
}
}
// ✅ 巨大JSONをストリームパーサーで処理
// JsonMachine ライブラリを使用(composer require halaxa/json-machine)
use JsonMachineItems;
$users = Items::fromFile('huge-data.json', ['pointer' => '/users']);
foreach ($users as $user) {
// 1件ずつ処理(メモリ消費一定)
processUser($user);
}
7-3. チャンク処理(Laravel chunk)
Laravelを使っている場合は、chunk() メソッドでデータを分割して処理できます。
PHP (Laravel) – chunk / lazy / cursor
<?php
// ❌ 全件取得 → メモリ不足
$users = User::all();
// ✅ chunk() で1000件ずつ処理
User::chunk(1000, function ($users) {
foreach ($users as $user) {
$user->sendNotification();
}
});
// ✅ lazy() でジェネレータベースの処理(Laravel 8+)
User::lazy()->each(function ($user) {
$user->sendNotification();
});
// ✅ cursor() で1件ずつフェッチ
foreach (User::cursor() as $user) {
$user->sendNotification();
}
// ✅ chunkById() でページネーション処理(IDが大きい場合に効率的)
User::chunkById(1000, function ($users) {
foreach ($users as $user) {
$user->update(['processed' => true]);
}
});
7-4. unset() でメモリ解放
PHP – unset() の効果的な使い方
<?php
// ✅ 大きなデータを処理したら即座に解放
function processLargeFile($path) {
$content = file_get_contents($path);
$data = json_decode($content, true);
unset($content); // 生データを解放
$result = transform($data);
unset($data); // 変換前データを解放
return $result;
}
// ✅ ループ内での解放
$files = glob('data/*.json');
foreach ($files as $file) {
$data = json_decode(file_get_contents($file), true);
processData($data);
unset($data); // 各ファイル処理後に解放
}
unset() の注意点
unset() は変数を削除するだけで、すぐにメモリが解放されるとは限りません(GCのタイミングによる)
- 参照カウントが0になったとき初めてメモリが解放されます
$var = null; も同様の効果がありますが、unset() の方が意図が明確です
8. 解決方法③:DBクエリの最適化
データベースからのデータ取得もメモリ不足の大きな原因です。クエリを最適化することで、メモリ消費を大幅に削減できます。
8-1. LIMIT / OFFSET でページネーション
PHP – LIMIT/OFFSET でチャンク処理
<?php
// ✅ LIMIT/OFFSET で分割取得
$batchSize = 1000;
$offset = 0;
do {
$stmt = $pdo->prepare('SELECT * FROM orders LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $batchSize, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = count($rows);
foreach ($rows as $row) {
processOrder($row);
}
unset($rows); // バッチ処理後に解放
$offset += $batchSize;
} while ($count === $batchSize);
注意:OFFSET が大きくなるとパフォーマンスが低下します(MySQLは OFFSET 分のレコードを読み飛ばすため)。大量データの場合は「カーソルベースのページネーション」を使いましょう。
8-2. カーソルベースのページネーション
PHP – カーソルベースのページネーション
<?php
// ✅ IDベースのカーソルページネーション(高速)
$batchSize = 1000;
$lastId = 0;
do {
$stmt = $pdo->prepare(
'SELECT * FROM orders WHERE id > :lastId ORDER BY id ASC LIMIT :limit'
);
$stmt->bindValue(':lastId', $lastId, PDO::PARAM_INT);
$stmt->bindValue(':limit', $batchSize, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = count($rows);
foreach ($rows as $row) {
processOrder($row);
$lastId = $row['id'];
}
unset($rows);
} while ($count === $batchSize);
8-3. 不要なカラムを取得しない
PHP – 必要なカラムだけ取得
<?php
// ❌ SELECT * で全カラム取得(TEXT/BLOBカラムを含むと大量メモリ消費)
$stmt = $pdo->query('SELECT * FROM articles');
// ✅ 必要なカラムだけ取得
$stmt = $pdo->query('SELECT id, title, status FROM articles');
// Laravel の場合
// ❌
$articles = Article::all();
// ✅
$articles = Article::select('id', 'title', 'status')->get();
9. WordPress でのメモリ不足
WordPressは多くのプラグインやテーマを使用するため、メモリ不足が発生しやすい環境です。WordPress固有の対処法を解説します。
9-1. WP_MEMORY_LIMIT / WP_MAX_MEMORY_LIMIT
WordPressには独自のメモリ制限設定があります。
wp-config.php
<?php
// フロントエンド(サイト表示)のメモリ上限
define('WP_MEMORY_LIMIT', '256M');
// 管理画面のメモリ上限(より大きく設定)
define('WP_MAX_MEMORY_LIMIT', '512M');
// ※ これらは wp-settings.php の前に記述すること
// require_once ABSPATH . 'wp-settings.php'; より上
| 設定 |
デフォルト値 |
適用範囲 |
推奨値 |
WP_MEMORY_LIMIT |
40M(シングル)/ 64M(マルチサイト) |
フロントエンド |
256M |
WP_MAX_MEMORY_LIMIT |
256M |
管理画面 |
512M |
注意:WP_MEMORY_LIMIT は php.ini の memory_limit を超えることはできません。例えば php.ini が 128M なら、WP_MEMORY_LIMIT = '256M' としても 128M のままです。必ず php.ini 側も変更してください。
9-2. プラグインが原因の場合
多数のプラグインを導入すると、各プラグインがメモリを消費します。
プラグインのメモリ消費を調査する方法
メモリ消費が大きいプラグインの特定方法:
1. 全プラグインを無効化
2. 1つずつ有効化してメモリ使用量を確認
3. メモリ消費が大きいプラグインを特定
確認方法:
- Query Monitor プラグインを使う
- wp-config.php に以下を追加:
define('SAVEQUERIES', true);
- フッターにメモリ使用量を表示:
echo 'メモリ: ' . round(memory_get_peak_usage() / 1024 / 1024, 2) . 'MB';
PHP – WordPress でメモリ使用量をフッターに表示
<?php
// functions.php に追加
add_action('wp_footer', function() {
if (current_user_can('manage_options')) {
$mem = round(memory_get_peak_usage() / 1024 / 1024, 2);
$limit = ini_get('memory_limit');
$queries = get_num_queries();
$time = timer_stop(0, 3);
echo "<!-- Memory: {$mem}MB / {$limit} | Queries: {$queries} | Time: {$time}s -->";
}
});
9-3. メモリ消費が大きい一般的なプラグイン
| プラグイン種類 |
メモリ消費傾向 |
対策 |
| ページビルダー(Elementor等) |
高い(50〜100MB) |
memory_limit 512M 推奨 |
| WooCommerce |
高い |
256M 以上推奨 |
| 多言語(WPML, Polylang) |
中〜高 |
言語数に比例 |
| 画像最適化 |
処理時に高い |
バッチ処理数を制限 |
| バックアップ系 |
実行時に非常に高い |
CLI での実行推奨 |
9-4. WordPress の画像アップロード時のエラー対策
wp-config.php – 画像アップロード時のメモリ対策
<?php
// wp-config.php に追加
// メモリ上限を引き上げ
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');
// 画像編集時のメモリ制限を緩和
// (WordPress は画像アップロード時にサムネイルを自動生成する)
PHP – functions.php でサムネイルサイズを制限
<?php
// 不要なサムネイルサイズを削除(メモリ節約)
add_action('init', function() {
remove_image_size('1536x1536');
remove_image_size('2048x2048');
});
// アップロード時の最大画像サイズを制限
add_filter('big_image_size_threshold', function() {
return 2560; // 最大2560pxにリサイズ
});
10. メモリ使用量の調査方法
メモリ不足の原因を特定するためには、どの処理でどれだけのメモリを消費しているかを計測することが重要です。
10-1. memory_get_usage() / memory_get_peak_usage()
PHP – メモリ使用量の計測
<?php
// memory_get_usage() - 現在のメモリ使用量
echo '現在: ' . memory_get_usage() . " bytes
";
// memory_get_usage(true) - OSから割り当てられた実メモリ量
echo '実メモリ: ' . memory_get_usage(true) . " bytes
";
// memory_get_peak_usage() - ピーク(最大)メモリ使用量
echo 'ピーク: ' . memory_get_peak_usage() . " bytes
";
// 人間が読みやすい形式で表示するヘルパー
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
return round($bytes / pow(1024, $pow), $precision) . ' ' . $units[$pow];
}
echo formatBytes(memory_get_usage()); // 例: 2.34 MB
echo formatBytes(memory_get_peak_usage()); // 例: 15.67 MB
10-2. 各処理のメモリ消費を計測
PHP – メモリプロファイラクラス
<?php
class MemoryProfiler {
private array $checkpoints = [];
public function checkpoint(string $label): void {
$this->checkpoints[] = [
'label' => $label,
'memory' => memory_get_usage(),
'peak' => memory_get_peak_usage(),
'time' => microtime(true),
];
}
public function report(): void {
echo str_pad('チェックポイント', 30) . ' | メモリ | 増減 | ピーク' . "
";
echo str_repeat('-', 75) . "
";
$prev = null;
foreach ($this->checkpoints as $cp) {
$diff = $prev ? $cp['memory'] - $prev['memory'] : 0;
$sign = $diff >= 0 ? '+' : '';
printf(
'%-30s | %-10s | %s%-8s | %s' . "
",
$cp['label'],
$this->format($cp['memory']),
$sign,
$this->format($diff),
$this->format($cp['peak'])
);
$prev = $cp;
}
}
private function format($bytes): string {
return round($bytes / 1024 / 1024, 2) . 'MB';
}
}
// 使用例
$profiler = new MemoryProfiler();
$profiler->checkpoint('開始');
$data = file_get_contents('large-file.json');
$profiler->checkpoint('ファイル読み込み後');
$parsed = json_decode($data, true);
$profiler->checkpoint('JSONデコード後');
unset($data);
$profiler->checkpoint('生データ解放後');
$profiler->report();
実行結果
チェックポイント | メモリ | 増減 | ピーク
---------------------------------------------------------------------------
開始 | 0.39MB | +0MB | 0.39MB
ファイル読み込み後 | 10.39MB | +10MB | 10.39MB
JSONデコード後 | 25.67MB | +15.28MB | 25.67MB
生データ解放後 | 15.67MB | -10MB | 25.67MB
10-3. Xdebug プロファイリング
Xdebugを使うと、関数レベルでのメモリ消費を詳細に分析できます。
php.ini – Xdebug プロファイリング設定
; Xdebug 3.x の設定
xdebug.mode = profile
xdebug.output_dir = /tmp/xdebug
xdebug.profiler_output_name = cachegrind.out.%p
; トリガーで有効化(常時有効にしない)
xdebug.start_with_request = trigger
; ブラウザに ?XDEBUG_PROFILE=1 を付けてアクセス
プロファイリング結果の確認方法
プロファイリング結果の確認ツール:
1. KCacheGrind(Linux)/ QCacheGrind(Windows/Mac)
- cachegrind.out.* ファイルを開いて視覚的に分析
2. Webgrind(Webブラウザベース)
- https://github.com/jokkedk/webgrind
- PHPのWebサーバーで動作
3. Blackfire.io(SaaS)
- より高度なプロファイリング
- 本番環境でも使用可能
10-4. PHPの組み込み関数でのメモリ監視
PHP – メモリ警告システム
<?php
// メモリ使用量が上限の80%を超えたら警告
function checkMemoryWarning($threshold = 0.8): void {
$limit = returnBytes(ini_get('memory_limit'));
if ($limit === -1) return; // 無制限
$usage = memory_get_usage(true);
$ratio = $usage / $limit;
if ($ratio >= $threshold) {
$usageMB = round($usage / 1024 / 1024, 1);
$limitMB = round($limit / 1024 / 1024, 1);
$pct = round($ratio * 100, 1);
error_log("WARNING: Memory usage {$usageMB}MB / {$limitMB}MB ({$pct}%)");
}
}
// ループ内で定期チェック
foreach ($largeDataset as $i => $item) {
processItem($item);
if ($i % 100 === 0) {
checkMemoryWarning();
}
}
11. 実務パターン集
実際の業務でよく遭遇するメモリ不足のシナリオと、その具体的な解決コードを紹介します。
11-1. CSVエクスポート(大量行)
PHP – 大量データのCSVエクスポート(省メモリ版)
<?php
// ✅ ストリーミングCSVエクスポート
function exportCsvStream(PDO $pdo, string $filename): void {
// ヘッダーを送信
header('Content-Type: text/csv; charset=UTF-8');
header("Content-Disposition: attachment; filename="{$filename}"");
header('Cache-Control: no-cache');
// php://output に直接書き込み
$output = fopen('php://output', 'w');
// BOM(Excelで文字化けしないように)
fwrite($output, "xEFxBBxBF");
// ヘッダー行
fputcsv($output, ['ID', '名前', 'メール', '登録日']);
// バッファリング無効化で1行ずつ送信
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $pdo->query('SELECT id, name, email, created_at FROM users ORDER BY id');
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
fputcsv($output, $row);
}
fclose($output);
}
// 使用例(100万行でもメモリ数MBで完了)
exportCsvStream($pdo, 'users-export.csv');
11-2. 画像一括リサイズ
PHP – 画像一括リサイズ(メモリ管理付き)
<?php
function batchResizeImages(string $dir, int $maxWidth = 1920): void {
$files = glob($dir . '/*.{jpg,jpeg,png}', GLOB_BRACE);
$total = count($files);
foreach ($files as $i => $file) {
echo "[" . ($i + 1) . "/{$total}] " . basename($file) . "...";
// メモリチェック
if (!checkImageMemory($file)) {
echo ' SKIPPED (メモリ不足)' . "
";
continue;
}
$info = getimagesize($file);
if ($info[0] <= $maxWidth) {
echo ' SKIPPED (既に小さい)' . "
";
continue;
}
// リサイズ実行
$image = imagecreatefromstring(file_get_contents($file));
$resized = imagescale($image, $maxWidth);
// 保存
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if ($ext === 'png') {
imagepng($resized, $file);
} else {
imagejpeg($resized, $file, 85);
}
// 即座にメモリ解放
imagedestroy($image);
imagedestroy($resized);
$mem = round(memory_get_usage() / 1024 / 1024, 1);
echo " DONE (Mem: {$mem}MB)
";
}
}
11-3. APIデータの大量取得
PHP – APIページネーションの省メモリ実装
<?php
// ✅ APIデータをジェネレータで取得
function fetchAllFromApi(string $baseUrl): Generator {
$page = 1;
$maxPages = 1000; // 安全制限
while ($page <= $maxPages) {
$url = "{$baseUrl}?page={$page}&per_page=100";
$response = file_get_contents($url);
$data = json_decode($response, true);
unset($response); // 生レスポンスを即解放
if (empty($data['items'])) break;
foreach ($data['items'] as $item) {
yield $item;
}
$page++;
unset($data);
}
}
// 使用例
foreach (fetchAllFromApi('https://api.example.com/users') as $user) {
saveUser($user);
}
11-4. PDFの生成
PHP – 大量ページのPDF生成
<?php
// ❌ 全ページを一括生成 → メモリ不足
$mpdf = new MpdfMpdf();
foreach ($allData as $item) {
$mpdf->WriteHTML(renderTemplate($item));
}
// ✅ テンポラリファイルを使ってページ分割
$mpdf = new MpdfMpdf([
'tempDir' => '/tmp/mpdf',
]);
// メモリ効率の良い設定
ini_set('memory_limit', '512M');
// チャンクでHTML生成
$chunkSize = 100;
$offset = 0;
$stmt = $pdo->prepare('SELECT * FROM invoices LIMIT :limit OFFSET :offset');
do {
$stmt->execute([':limit' => $chunkSize, ':offset' => $offset]);
$rows = $stmt->fetchAll();
$count = count($rows);
$html = '';
foreach ($rows as $row) {
$html .= renderInvoice($row);
}
$mpdf->WriteHTML($html);
unset($html, $rows);
$offset += $chunkSize;
} while ($count === $chunkSize);
$mpdf->Output('invoices.pdf', 'D');
11-5. ログファイルの解析
PHP – 巨大ログファイルの解析
<?php
// ✅ ストリーム処理でログ解析
function analyzeLog(string $logPath): array {
$stats = [
'total_lines' => 0,
'errors' => 0,
'warnings' => 0,
'error_types' => [],
];
$handle = fopen($logPath, 'r');
while (($line = fgets($handle)) !== false) {
$stats['total_lines']++;
if (strpos($line, 'ERROR') !== false) {
$stats['errors']++;
// エラータイプを集計
if (preg_match('/ERRORs+(w+)/', $line, $m)) {
$type = $m[1];
$stats['error_types'][$type] = ($stats['error_types'][$type] ?? 0) + 1;
}
} elseif (strpos($line, 'WARNING') !== false) {
$stats['warnings']++;
}
}
fclose($handle);
arsort($stats['error_types']);
return $stats;
}
// 5GBのログファイルでもメモリ数MBで解析可能
$result = analyzeLog('/var/log/app/production.log');
print_r($result);
11-6. Excelファイルの大量処理(PhpSpreadsheet)
PHP – PhpSpreadsheet の省メモリ設定
<?php
use PhpOfficePhpSpreadsheetIOFactory;
use PhpOfficePhpSpreadsheetSettings;
use PhpOfficePhpSpreadsheetCellStringValueBinder;
// ✅ メモリキャッシュを使用(デフォルトはインメモリ)
$cache = new PhpOfficePhpSpreadsheetCollectionMemorySimpleCache3();
Settings::setCache($cache);
// ✅ 読み込みフィルターで必要なセルだけ読む
class ColumnFilter implements PhpOfficePhpSpreadsheetReaderIReadFilter {
public function readCell($column, $row, $worksheetName = ''): bool {
// A〜E列のみ読み込み
return in_array($column, ['A', 'B', 'C', 'D', 'E']);
}
}
$reader = IOFactory::createReaderForFile('huge-data.xlsx');
$reader->setReadFilter(new ColumnFilter());
$reader->setReadDataOnly(true); // 書式を読まない
$spreadsheet = $reader->load('huge-data.xlsx');
| PhpSpreadsheetの最適化 |
メモリ削減効果 |
setReadDataOnly(true) |
書式データ分を削減(約30〜50%) |
| ReadFilter で列制限 |
不要列分を削減 |
| チャンク読み込み |
行数に依存しなくなる |
| CSVに変換して処理 |
大幅に削減 |
12. まとめ
12-1. 原因チェックリスト
Fatal error: Allowed memory size exhausted が発生したら、以下のチェックリストで原因を特定しましょう。
メモリ不足 原因チェックリスト
- 大量データの読み込み: file_get_contents / file() / fetchAll() で巨大データを一括読み込みしていないか?
- 無限ループ: while/for の終了条件は正しいか?カウンタを正しくインクリメントしているか?
- 再帰の深さ: 再帰関数に終了条件はあるか?深さ制限を設けているか?
- 循環参照: オブジェクト同士が互いを参照していないか?
- 画像処理: 大きな画像をGDで直接処理していないか?メモリ見積もりをしているか?
- 配列の肥大化: ループ内で配列にデータを追加し続けていないか?
- 不要な変数: 使い終わった大きなデータを unset() しているか?
- プラグイン(WordPress): 不要なプラグインを無効化しているか?
12-2. memory_limit 推奨値表
| 用途 |
推奨 memory_limit |
コード最適化が必要か |
| 一般的なWebページ表示 |
128M |
通常不要 |
| WordPress(10個以内のプラグイン) |
256M |
通常不要 |
| WordPress + WooCommerce |
256M〜512M |
商品数による |
| CSV処理(〜1万行) |
128M |
不要 |
| CSV処理(10万行以上) |
256M〜512M |
必要(ストリーム処理推奨) |
| 画像処理(〜2000px) |
128M |
不要 |
| 画像処理(4000px以上) |
512M〜1G |
必要(Imagick/exec推奨) |
| PDF大量生成 |
512M |
必要(チャンク処理推奨) |
12-3. 解決フローチャート
解決フローチャート
Fatal error: Allowed memory size exhausted 発生
│
├─ まず「tried to allocate Y bytes」を確認
│ ├─ Y が小さい(数KB〜数MB)→ 徐々にメモリが増えた → ループ/配列肥大化を疑う
│ └─ Y が大きい(数十MB〜)→ 一発でメモリを食った → 画像処理/巨大ファイル読み込みを疑う
│
├─ 応急処置: memory_limit を増やす
│ ├─ php.ini / .htaccess / ini_set()
│ └─ WordPress: wp-config.php の WP_MEMORY_LIMIT
│
├─ 根本対策: コードを最適化
│ ├─ ファイル読み込み → ストリーム処理(fgets / SplFileObject)
│ ├─ DB大量取得 → fetch() / LIMIT+OFFSET / カーソル
│ ├─ 配列肥大化 → ジェネレータ(yield) / unset()
│ ├─ 画像処理 → メモリ見積もり / Imagick / exec
│ └─ Laravel → chunk() / lazy() / cursor()
│
└─ 調査: memory_get_usage() でボトルネックを特定
12-4. よくある質問(FAQ)
Q. memory_limit を -1(無制限)にしても大丈夫?
開発環境では問題ありませんが、本番環境では推奨されません。PHPスクリプトがサーバーの物理メモリをすべて使い切ると、他のプロセス(MySQL、Apache等)に影響し、サーバー全体がダウンする可能性があります。本番環境では適切な上限を設定し、コードの最適化で対応しましょう。
Q. ini_set() で memory_limit を変更しても反映されない
以下の原因が考えられます:
- レンタルサーバーが
ini_set() を無効化している
- Suhosin パッチにより制限されている
- PHP-FPM の設定で上書きされている
- サーバーの管理パネルで上限が設定されている
サーバーの管理パネルか php.ini で直接設定してください。
Q. メモリ不足で画面が真っ白になる
Fatal error が発生するとスクリプトが停止し、何も出力されないため画面が真っ白になります。対処法:
php.ini で display_errors = On に設定してエラーを表示
- エラーログ(
error_log)を確認
- WordPressの場合:
wp-config.php に define('WP_DEBUG', true); を追加
Q. WordPress のプラグイン更新時にメモリ不足になる
プラグインの更新やインストールには通常より多くのメモリが必要です。wp-config.php で WP_MAX_MEMORY_LIMIT を 512M 以上に設定してください。それでも解決しない場合は、WP-CLI を使ってコマンドラインから更新すると、CLI モードではメモリ制限が緩いため成功しやすくなります: wp plugin update --all
Q. unset() してもメモリが減らない
unset() は変数を削除しますが、実際のメモリ解放はPHPのガベージコレクタのタイミングに依存します。以下の点を確認してください:
- 他の変数がそのデータを参照していないか(参照カウントが0にならないとGCされない)
- 循環参照がないか(
gc_collect_cycles() を手動実行)
memory_get_usage(true) は OS レベルの割り当てを示すため、PHP内部の解放がすぐに反映されない場合がある
この記事のまとめ
Fatal error: Allowed memory size exhausted は PHPの memory_limit を超えると発生する
- 主な原因は「大量データの一括読み込み」「無限ループ」「画像処理」「配列の肥大化」
- 応急処置として memory_limit を増やせるが、根本的にはコードの最適化が必要
- ジェネレータ(yield)とストリーム処理を活用すれば、データ量に依存しない省メモリなコードが書ける
- WordPressでは
WP_MEMORY_LIMIT と WP_MAX_MEMORY_LIMIT を wp-config.php で設定
memory_get_usage() で計測し、ボトルネックを特定してから対策する