T-CREATOR

PHP で「Allowed memory size exhausted」発生時の原因と対処【完全解説】

PHP で「Allowed memory size exhausted」発生時の原因と対処【完全解説】

PHP でアプリケーションを開発していると、突然「Allowed memory size exhausted」というエラーに遭遇することがあります。このエラーは、処理中に PHP のメモリ制限を超えてしまった際に発生するもので、初心者にとっては原因の特定が難しいことも少なくありません。

本記事では、このエラーが発生する背景から具体的な対処方法まで、実践的な内容を丁寧に解説していきます。エラーメッセージの読み方、原因の特定方法、そして効果的な解決策を段階的に学んでいきましょう。

背景

PHP のメモリ管理の仕組み

PHP は、スクリプト実行時に使用できるメモリ量を制限する仕組みを持っています。これは、サーバーのリソースを保護し、暴走したスクリプトがシステム全体に影響を与えることを防ぐための重要な機能です。

PHP のメモリ管理は、php.iniファイルのmemory_limitディレクティブによって制御されています。デフォルトでは 128MB に設定されていることが多いですが、サーバー環境によって異なります。

以下の図は、PHP スクリプト実行時のメモリ使用の流れを示しています。

mermaidflowchart TB
    start["スクリプト開始"] --> init["メモリ初期化"]
    init --> check1{"現在のメモリ使用量"}
    check1 -->|制限内| process["処理実行<br/>・データ読み込み<br/>・配列操作<br/>・オブジェクト生成"]
    check1 -->|制限超過| error["Fatal Error:<br/>Allowed memory size exhausted"]
    process --> check2{"メモリ制限チェック"}
    check2 -->|制限内| process
    check2 -->|制限超過| error
    process --> complete["処理完了<br/>メモリ解放"]
    error --> terminate["スクリプト終了"]
    complete --> terminate

図で理解できる要点:

  • PHP は処理の各段階でメモリ使用量を監視しています
  • 制限を超えた瞬間に Fatal Error が発生します
  • 処理完了後は自動的にメモリが解放されます

メモリ制限が設定されている理由

メモリ制限は、以下のような理由で設定されています。

1 つ目は、サーバーリソースの保護です。共有ホスティング環境では、複数のユーザーが同じサーバーを使用するため、1 つのスクリプトが大量のメモリを消費すると他のユーザーに影響が出てしまいます。

2 つ目は、無限ループや暴走の防止でしょう。プログラムのバグによって無限にデータを蓄積し続けるような状況を防ぐことができます。

3 つ目は、パフォーマンスの維持ですね。メモリを大量に消費する処理は、サーバー全体のパフォーマンスを低下させる可能性があるため、制限を設けることで安定した動作を保証します。

現在のメモリ制限を確認する方法

PHP で現在設定されているメモリ制限を確認する方法は、いくつか存在します。

方法 1: phpinfo()を使用する

最も確実な方法は、phpinfo()関数を使用することです。

php<?php
// phpinfo.phpファイルを作成
phpinfo();
?>

このファイルをブラウザで開くと、PHP 設定の詳細情報が表示されます。「memory_limit」という項目を探してください。

方法 2: ini_get()を使用する

プログラム内で動的に確認したい場合は、ini_get()関数が便利です。

php<?php
// 現在のメモリ制限を取得
$memory_limit = ini_get('memory_limit');
echo "現在のメモリ制限: {$memory_limit}\n";

// 現在のメモリ使用量を取得
$memory_usage = memory_get_usage(true);
$memory_usage_mb = round($memory_usage / 1024 / 1024, 2);
echo "現在のメモリ使用量: {$memory_usage_mb} MB\n";
?>

この方法を使うと、スクリプト実行中にリアルタイムでメモリの状況を監視できます。

方法 3: コマンドラインから確認する

サーバーに SSH でアクセスできる場合は、以下のコマンドで確認できます。

bash# php.iniファイルの場所を確認
php --ini

# memory_limitの値を直接確認
php -r "echo ini_get('memory_limit');"

これらの方法を使い分けることで、開発環境や本番環境のメモリ設定を適切に把握できますね。

課題

「Allowed memory size exhausted」エラーの詳細

このエラーが発生すると、PHP スクリプトは即座に停止し、以下のようなエラーメッセージが表示されます。

vbnetFatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) in /path/to/script.php on line 42

エラーコード: Fatal error(致命的エラー)

エラーメッセージの構成:

  • Allowed memory size of 134217728 bytes exhausted: 128MB(134217728 バイト)のメモリ制限に達した
  • tried to allocate 20480 bytes: さらに 20KB(20480 バイト)のメモリを確保しようとした
  • in ​/​path​/​to​/​script.php on line 42: エラーが発生したファイルと行番号

このエラーは、スクリプトが設定されたメモリ制限を超えてメモリを使用しようとした際に発生します。重要なのは、エラーメッセージに表示される「tried to allocate」の値は、実際に不足しているメモリ量ではなく、最後に確保しようとした量だということです。

以下の図は、メモリ不足が発生する典型的なパターンを示しています。

mermaidflowchart LR
    pattern1["大量データ読み込み"] -->|原因| issue1["配列に全データ格納"]
    pattern2["ファイル処理"] -->|原因| issue2["file_get_contents()で<br/>大容量ファイル読み込み"]
    pattern3["画像処理"] -->|原因| issue3["高解像度画像の<br/>メモリ展開"]
    pattern4["無限ループ"] -->|原因| issue4["ループ内でデータ蓄積"]
    pattern5["ライブラリの過剰使用"] -->|原因| issue5["多数のオブジェクト生成"]

    issue1 & issue2 & issue3 & issue4 & issue5 --> error["Fatal Error:<br/>Memory Exhausted"]

図で理解できる要点:

  • メモリ不足には複数の典型的なパターンが存在します
  • どのパターンも最終的には同じエラーに到達します
  • 原因パターンを理解することで対処が容易になります

メモリ不足が発生する主な原因

原因 1: 大量のデータを一度に読み込む

データベースから数万件のレコードを一度に取得して配列に格納するような処理は、メモリを大量に消費します。

php<?php
// ❌ 悪い例: 全データを一度に取得
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$stmt = $pdo->query('SELECT * FROM large_table'); // 10万件のレコード
$all_data = $stmt->fetchAll(PDO::FETCH_ASSOC); // すべてをメモリに展開

// この時点でメモリを大量に消費
foreach ($all_data as $row) {
    // 処理
}
?>

この例では、10 万件のレコードをすべてメモリ上に展開しているため、大量のメモリを消費してしまいます。

原因 2: ファイル処理での不適切な読み込み

大きなファイルをfile_get_contents()file()で一度に読み込むと、ファイルサイズ分のメモリが必要になります。

php<?php
// ❌ 悪い例: 500MBのファイルを一度に読み込み
$large_file = '/path/to/large_file.csv'; // 500MBのCSVファイル
$content = file_get_contents($large_file); // 500MB分のメモリを消費

// ファイル内容を行ごとに処理
$lines = explode("\n", $content); // さらにメモリを消費
?>

原因 3: 画像処理での高解像度画像の展開

画像処理ライブラリ(GD や Imagick)は、画像をメモリ上に展開するため、高解像度の画像では大量のメモリを必要とします。

php<?php
// ❌ 悪い例: 高解像度画像の処理
$image_path = '/path/to/high_resolution.jpg'; // 4000x3000ピクセル
$image = imagecreatefromjpeg($image_path);

// 4000x3000x3(RGB)x4(32bit) = 約144MB のメモリを消費
imagejpeg($image, '/path/to/output.jpg', 90);
imagedestroy($image);
?>

計算式を見ると、なぜ画像処理でメモリを大量に消費するのかが理解できますね。

原因 4: 無限ループやメモリリークでのメモリ蓄積

ループ内で変数を蓄積し続けると、メモリが解放されずに増え続けてしまいます。

php<?php
// ❌ 悪い例: ループ内でのデータ蓄積
$results = [];
for ($i = 0; $i < 100000; $i++) {
    // 毎回新しいオブジェクトを作成し、配列に追加
    $results[] = new LargeObject($i);
    // $resultsは解放されないため、メモリが増え続ける
}
?>

原因 5: 外部ライブラリの過剰なメモリ消費

Composer などでインストールしたライブラリやフレームワークの Autoload が、多数のクラスファイルをメモリに読み込むことでメモリを消費することもあります。

以下の表は、各原因とその典型的なメモリ消費量をまとめたものです。

#原因典型的なメモリ消費量リスクレベル
1大量データの一括読み込み10MB〜1GB+★★★
2大容量ファイルの読み込みファイルサイズと同等★★★
3高解像度画像の処理50MB〜500MB★★★
4無限ループ・メモリリーク時間経過で増加★★★
5ライブラリの過剰使用数 MB〜数十 MB★★☆

エラー発生時の影響範囲

このエラーが発生すると、以下のような影響が出ます。

処理の即時停止: スクリプトは即座に終了し、それ以降の処理は実行されません。データベースのトランザクション中であれば、ロールバックされない可能性もあります。

ユーザーへの影響: Web アプリケーションの場合、ユーザーにはエラーページが表示されるか、真っ白な画面(White Screen of Death)が表示されてしまいます。

ログの肥大化: エラーが頻繁に発生すると、エラーログファイルが肥大化し、ディスク容量を圧迫することもありますね。

解決策

解決策の全体フロー

メモリ不足エラーに対処するには、大きく分けて 2 つのアプローチがあります。1 つはメモリ制限を増やす方法、もう 1 つはコードを最適化してメモリ使用量を減らす方法です。

以下の図は、エラー発生時の対処フローを示しています。

mermaidflowchart TD
    error["メモリエラー発生"] --> analyze["エラー原因の分析"]
    analyze --> check["メモリ使用状況の確認"]
    check --> decide{"根本原因は?"}

    decide -->|"処理の性質上<br/>大量メモリが必要"| increase["メモリ制限の増加"]
    decide -->|"コードの問題"| optimize["コードの最適化"]

    increase --> temp["一時的な対処"]
    temp --> method1["ini_set()で増加"]
    temp --> method2["php.iniで増加"]
    temp --> method3[".htaccessで増加"]

    optimize --> perm["恒久的な対処"]
    perm --> opt1["ストリーミング処理"]
    perm --> opt2["チャンク処理"]
    perm --> opt3["メモリ解放"]
    perm --> opt4["データ構造の見直し"]

    method1 & method2 & method3 --> verify["動作確認"]
    opt1 & opt2 & opt3 & opt4 --> verify
    verify --> done["解決"]

図で理解できる要点:

  • まずエラーの根本原因を特定することが重要です
  • 一時的な対処と恒久的な対処を使い分けます
  • 複数の解決方法を組み合わせることも可能です

解決策 1: メモリ制限を増やす

方法 A: スクリプト内で ini_set()を使用する

最も手軽な方法は、スクリプトの先頭でini_set()関数を使用してメモリ制限を増やすことです。

php<?php
// スクリプトの先頭に記述
ini_set('memory_limit', '256M'); // 256MBに増やす

// または無制限にする(本番環境では非推奨)
// ini_set('memory_limit', '-1');

// 設定が反映されたか確認
echo "現在のメモリ制限: " . ini_get('memory_limit') . "\n";
?>

注意点: この方法は、サーバーの設定でphp_admin_valueが使用されている場合は効果がありません。

方法 B: php.ini ファイルを編集する

サーバー全体でメモリ制限を変更したい場合は、php.iniファイルを編集します。

ini; php.iniファイル内の設定
memory_limit = 256M

変更後は、Web サーバー(Apache や Nginx)を再起動する必要があります。

bash# Apacheの再起動
sudo systemctl restart apache2

# Nginxの場合はPHP-FPMを再起動
sudo systemctl restart php-fpm

方法 C: .htaccess ファイルで設定する

Apache を使用している場合は、.htaccessファイルで設定することも可能です。

apache# .htaccessファイルに追加
php_value memory_limit 256M

この方法は、特定のディレクトリだけメモリ制限を変更したい場合に便利ですね。

メモリ制限増加時の推奨値

#用途推奨メモリ制限備考
1通常の Web アプリケーション128MB〜256MBデフォルト設定
2大量データ処理512MB〜1GBバッチ処理など
3画像処理・PDF 生成256MB〜512MB高解像度対応
4インポート・エクスポート512MB〜2GB一時的な処理
5開発環境512MB〜無制限デバッグ目的

解決策 2: コードを最適化する(推奨)

メモリ制限を増やすだけでは根本的な解決にはなりません。コードを最適化してメモリ使用量を減らすことが、最も効果的で持続可能な解決策です。

最適化 A: データベースクエリをストリーミング処理する

大量のデータを扱う場合は、一度にすべてを読み込むのではなく、少しずつ処理する方法に変更します。

php<?php
// ✅ 良い例: ストリーミング処理
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$stmt = $pdo->query('SELECT * FROM large_table');

// 1行ずつ取得して処理
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // 処理
    processRow($row);

    // 処理が終わった行は自動的にメモリから解放される
}
?>

この方法では、常に 1 行分のデータだけがメモリに存在するため、メモリ使用量を大幅に削減できます。

さらに効率的に処理したい場合は、チャンクごとに取得する方法もあります。

php<?php
// ✅ 良い例: チャンク単位での処理
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$chunk_size = 1000; // 1000件ずつ処理
$offset = 0;

while (true) {
    $stmt = $pdo->prepare(
        'SELECT * FROM large_table LIMIT :limit OFFSET :offset'
    );
    $stmt->bindValue(':limit', $chunk_size, PDO::PARAM_INT);
    $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
    $stmt->execute();

    $chunk = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // データがなくなったらループを抜ける
    if (empty($chunk)) {
        break;
    }

    // チャンクを処理
    foreach ($chunk as $row) {
        processRow($row);
    }

    $offset += $chunk_size;
    unset($chunk); // 明示的にメモリを解放
}
?>

最適化 B: ファイルを行ごとに読み込む

大きなファイルを扱う場合は、fopen()fgets()を使用して行ごとに処理します。

php<?php
// ✅ 良い例: ファイルのストリーミング読み込み
$file_path = '/path/to/large_file.csv';
$handle = fopen($file_path, 'r');

if ($handle) {
    // 1行ずつ読み込む
    while (($line = fgets($handle)) !== false) {
        // CSV行を処理
        $data = str_getcsv($line);
        processData($data);
    }

    fclose($handle);
}
?>

この方法では、ファイルサイズに関わらず、常に 1 行分のデータだけがメモリに存在するため、メモリ効率が非常に良くなります。

最適化 C: 画像処理でのメモリ管理

画像処理では、処理が終わったらすぐにimagedestroy()でメモリを解放することが重要です。

php<?php
// ✅ 良い例: 画像処理後のメモリ解放
$image_path = '/path/to/image.jpg';

// 画像を読み込み
$source = imagecreatefromjpeg($image_path);

// リサイズ処理
$width = 800;
$height = 600;
$resized = imagecreatetruecolor($width, $height);
imagecopyresampled(
    $resized, $source,
    0, 0, 0, 0,
    $width, $height,
    imagesx($source), imagesy($source)
);

// 出力
imagejpeg($resized, '/path/to/output.jpg', 90);

// 明示的にメモリを解放
imagedestroy($source);
imagedestroy($resized);
?>

複数の画像を処理する場合は、1 つずつ処理してメモリを解放するようにしましょう。

php<?php
// ✅ 良い例: 複数画像の逐次処理
$image_files = glob('/path/to/images/*.jpg');

foreach ($image_files as $image_file) {
    // 画像を処理
    $image = imagecreatefromjpeg($image_file);

    // 処理(リサイズ、フィルタなど)
    processImage($image);

    // 保存
    imagejpeg($image, '/path/to/processed/' . basename($image_file));

    // 必ず解放
    imagedestroy($image);
    unset($image); // 変数も解放
}
?>

最適化 D: ループ内でのメモリ管理

ループ内で大きな変数を使用する場合は、ループの最後でunset()を使用してメモリを解放します。

php<?php
// ✅ 良い例: ループ内でのメモリ解放
for ($i = 0; $i < 10000; $i++) {
    // 大きなデータを処理
    $large_data = fetchLargeData($i);

    // 処理
    processData($large_data);

    // 明示的にメモリを解放
    unset($large_data);
}
?>

配列を使用する場合も、不要になったら解放することが重要です。

php<?php
// ✅ 良い例: 配列のメモリ管理
$data_chunks = range(1, 100);

foreach ($data_chunks as $chunk_id) {
    // チャンクデータを取得
    $chunk_data = fetchChunkData($chunk_id);

    // 処理
    foreach ($chunk_data as $item) {
        processItem($item);
    }

    // チャンクデータを解放
    unset($chunk_data);
}
?>

最適化 E: ジェネレータを活用する

PHP 5.5 以降では、ジェネレータを使用することで、メモリ効率の良いコードを書くことができます。

php<?php
// ✅ 良い例: ジェネレータの使用
function getDataGenerator($pdo) {
    $stmt = $pdo->query('SELECT * FROM large_table');

    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        yield $row; // 1行ずつ返す
    }
}

// 使用例
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');

foreach (getDataGenerator($pdo) as $row) {
    // 処理
    processRow($row);
    // $rowは次のループで自動的に解放される
}
?>

ジェネレータを使用すると、大量のデータを扱う場合でも、常に 1 つの要素だけがメモリに存在するため、非常にメモリ効率が良くなりますね。

ファイル読み込みでもジェネレータが活用できます。

php<?php
// ✅ 良い例: ファイル読み込みジェネレータ
function readLargeFile($file_path) {
    $handle = fopen($file_path, 'r');

    if ($handle) {
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }

        fclose($handle);
    }
}

// 使用例
foreach (readLargeFile('/path/to/large_file.csv') as $line) {
    $data = str_getcsv($line);
    processData($data);
}
?>

解決策 3: メモリ使用状況を監視する

メモリ問題を早期に発見するために、メモリ使用状況を監視する仕組みを実装することも重要です。

メモリ監視の実装

php<?php
/**
 * メモリ使用状況を監視するクラス
 */
class MemoryMonitor {
    private $checkpoint_data = [];

    /**
     * チェックポイントを作成
     */
    public function checkpoint($label) {
        $this->checkpoint_data[$label] = [
            'usage' => memory_get_usage(true),
            'peak' => memory_get_peak_usage(true),
            'time' => microtime(true)
        ];
    }

    /**
     * レポートを出力
     */
    public function report() {
        $limit = ini_get('memory_limit');
        echo "メモリ制限: {$limit}\n";
        echo str_repeat('-', 60) . "\n";

        foreach ($this->checkpoint_data as $label => $data) {
            $usage_mb = round($data['usage'] / 1024 / 1024, 2);
            $peak_mb = round($data['peak'] / 1024 / 1024, 2);

            echo sprintf(
                "%s: 使用量 %s MB / ピーク %s MB\n",
                $label, $usage_mb, $peak_mb
            );
        }
    }
}
?>

このクラスを使用して、処理の各段階でメモリ使用状況を記録できます。

php<?php
// 使用例
$monitor = new MemoryMonitor();

$monitor->checkpoint('開始時');

// データベースからデータを取得
$data = fetchLargeData();
$monitor->checkpoint('データ取得後');

// データを処理
processData($data);
$monitor->checkpoint('データ処理後');

// メモリを解放
unset($data);
$monitor->checkpoint('メモリ解放後');

// レポートを出力
$monitor->report();
?>

具体例

実践例 1: 大量データの CSV エクスポート

よくある例として、データベースから大量のデータを取得して CSV ファイルにエクスポートする処理を見てみましょう。

問題のあるコード

php<?php
// ❌ 悪い例: すべてのデータを一度にメモリに読み込む
$pdo = new PDO('mysql:host=localhost;dbname=sales', 'user', 'pass');

// 全レコードを取得(10万件)
$stmt = $pdo->query('SELECT * FROM orders');
$all_orders = $stmt->fetchAll(PDO::FETCH_ASSOC);

// CSVファイルを作成
$csv_content = '';
foreach ($all_orders as $order) {
    $csv_content .= implode(',', $order) . "\n";
}

file_put_contents('/path/to/export.csv', $csv_content);

// この時点で「Allowed memory size exhausted」が発生
?>

発生条件: 10 万件のデータを一度にメモリに展開し、さらに CSV 文字列も蓄積するため、メモリを大量に消費します。

改善後のコード

以下は、ストリーミング処理とチャンク処理を組み合わせた最適化版です。

php<?php
// ✅ 良い例: ストリーミング処理でCSVを出力
$pdo = new PDO('mysql:host=localhost;dbname=sales', 'user', 'pass');

// 出力バッファを無効化
if (ob_get_level()) {
    ob_end_clean();
}

// CSVファイルを開く
$output = fopen('/path/to/export.csv', 'w');

// ヘッダー行を書き込み
$headers = ['order_id', 'customer_name', 'amount', 'order_date'];
fputcsv($output, $headers);
?>

続いて、データを 1 行ずつ処理して CSV に書き込みます。

php<?php
// データを1行ずつ取得して処理
$stmt = $pdo->query('SELECT * FROM orders');

while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // CSVファイルに1行書き込み
    fputcsv($output, $row);

    // $rowは次のループで自動的に解放される
}

// ファイルを閉じる
fclose($output);

echo "エクスポート完了\n";
?>

解決方法のポイント:

  1. fetchAll()ではなくfetch()を使用して 1 行ずつ処理
  2. CSV 文字列を蓄積せず、直接ファイルに書き込み
  3. fputcsv()を使用することで、適切なエスケープ処理も自動化

この方法では、メモリ使用量は常に数 MB 程度に抑えられます。

実践例 2: 画像の一括リサイズ処理

複数の画像ファイルを一括でリサイズする処理も、メモリ不足エラーが発生しやすい処理です。

問題のあるコード

php<?php
// ❌ 悪い例: すべての画像を同時にメモリに読み込む
$image_files = glob('/path/to/images/*.jpg'); // 1000枚の画像
$images = [];

// すべての画像を読み込む
foreach ($image_files as $file) {
    $images[] = imagecreatefromjpeg($file);
}

// すべての画像をリサイズ
foreach ($images as $index => $image) {
    $resized = imagescale($image, 800, 600);
    imagejpeg($resized, "/path/to/resized/image_{$index}.jpg");
    imagedestroy($resized);
}

// メモリ解放を忘れている
// この時点で「Allowed memory size exhausted」が発生
?>

発生条件: 1000 枚の画像をすべてメモリに読み込むため、数 GB 以上のメモリを消費してしまいます。

改善後のコード(基本版)

php<?php
// ✅ 良い例: 1枚ずつ処理してメモリを解放
$image_files = glob('/path/to/images/*.jpg');
$output_dir = '/path/to/resized/';
$target_width = 800;
$target_height = 600;

foreach ($image_files as $file) {
    // 1枚の画像を読み込む
    $source = imagecreatefromjpeg($file);

    if ($source === false) {
        error_log("画像の読み込みに失敗: {$file}");
        continue;
    }

    // リサイズ
    $resized = imagescale($source, $target_width, $target_height);

    // 保存
    $output_path = $output_dir . basename($file);
    imagejpeg($resized, $output_path, 90);

    // メモリを解放
    imagedestroy($source);
    imagedestroy($resized);
    unset($source, $resized);
}

echo "リサイズ完了\n";
?>

さらに進んで、進捗表示とエラーハンドリングを追加した実践的なコードを見てみましょう。

php<?php
// ✅ 良い例: 進捗表示とエラーハンドリング付き
function resizeImages($input_dir, $output_dir, $width, $height) {
    $image_files = glob($input_dir . '/*.{jpg,jpeg,png}', GLOB_BRACE);
    $total = count($image_files);
    $processed = 0;
    $errors = [];

    foreach ($image_files as $file) {
        try {
            // 拡張子に応じて画像を読み込む
            $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));

            switch ($ext) {
                case 'jpg':
                case 'jpeg':
                    $source = imagecreatefromjpeg($file);
                    break;
                case 'png':
                    $source = imagecreatefrompng($file);
                    break;
                default:
                    throw new Exception("非対応の形式: {$ext}");
            }

            if ($source === false) {
                throw new Exception("画像の読み込みに失敗");
            }

            // リサイズ
            $resized = imagescale($source, $width, $height);

            // 保存
            $output_path = $output_dir . '/' . basename($file);
            imagejpeg($resized, $output_path, 90);

            // メモリ解放
            imagedestroy($source);
            imagedestroy($resized);
            unset($source, $resized);

            $processed++;

            // 進捗表示(10%ごと)
            $progress = round(($processed / $total) * 100);
            if ($processed % max(1, floor($total / 10)) === 0) {
                echo "進捗: {$progress}% ({$processed}/{$total})\n";
            }

        } catch (Exception $e) {
            $errors[] = basename($file) . ': ' . $e->getMessage();
        }
    }

    // 結果を返す
    return [
        'total' => $total,
        'processed' => $processed,
        'errors' => $errors
    ];
}

// 使用例
$result = resizeImages(
    '/path/to/images',
    '/path/to/resized',
    800,
    600
);

echo "\n処理完了\n";
echo "総数: {$result['total']}枚\n";
echo "成功: {$result['processed']}枚\n";
echo "失敗: " . count($result['errors']) . "枚\n";

if (!empty($result['errors'])) {
    echo "\nエラー詳細:\n";
    foreach ($result['errors'] as $error) {
        echo "  - {$error}\n";
    }
}
?>

実践例 3: ログファイルの解析

数 GB にもなる大きなログファイルを解析する処理も、メモリ効率を考慮する必要があります。

問題のあるコード

php<?php
// ❌ 悪い例: ログファイル全体を読み込む
$log_file = '/var/log/application.log'; // 2GBのログファイル
$log_content = file_get_contents($log_file); // 2GB分のメモリを消費

// 行ごとに分割
$lines = explode("\n", $log_content); // さらにメモリを消費

// エラー行をカウント
$error_count = 0;
foreach ($lines as $line) {
    if (strpos($line, 'ERROR') !== false) {
        $error_count++;
    }
}

echo "エラー数: {$error_count}\n";

// この時点で「Allowed memory size exhausted」が発生
?>

改善後のコード

php<?php
// ✅ 良い例: ログファイルを1行ずつ処理
$log_file = '/var/log/application.log';

// 統計情報を格納する配列
$stats = [
    'total_lines' => 0,
    'error_count' => 0,
    'warning_count' => 0,
    'info_count' => 0
];

// ファイルを開く
$handle = fopen($log_file, 'r');

if ($handle) {
    // 1行ずつ読み込む
    while (($line = fgets($handle)) !== false) {
        $stats['total_lines']++;

        // ログレベルをカウント
        if (strpos($line, 'ERROR') !== false) {
            $stats['error_count']++;
        } elseif (strpos($line, 'WARNING') !== false) {
            $stats['warning_count']++;
        } elseif (strpos($line, 'INFO') !== false) {
            $stats['info_count']++;
        }
    }

    fclose($handle);
}

// 結果を表示
echo "ログ解析結果:\n";
echo "総行数: " . number_format($stats['total_lines']) . "\n";
echo "ERROR: " . number_format($stats['error_count']) . "\n";
echo "WARNING: " . number_format($stats['warning_count']) . "\n";
echo "INFO: " . number_format($stats['info_count']) . "\n";
?>

さらに高度な処理として、正規表現でのパターンマッチングを追加した例です。

php<?php
// ✅ 良い例: 正規表現でエラーパターンを抽出
$log_file = '/var/log/application.log';
$error_patterns = [];

$handle = fopen($log_file, 'r');

if ($handle) {
    while (($line = fgets($handle)) !== false) {
        // エラー行を正規表現で解析
        if (preg_match('/ERROR.*?\[(\w+)\]/', $line, $matches)) {
            $error_type = $matches[1];

            // エラータイプごとにカウント
            if (!isset($error_patterns[$error_type])) {
                $error_patterns[$error_type] = 0;
            }
            $error_patterns[$error_type]++;
        }
    }

    fclose($handle);
}

// エラータイプごとの集計を表示
echo "エラータイプ別集計:\n";
arsort($error_patterns); // 多い順にソート

foreach ($error_patterns as $type => $count) {
    echo "  {$type}: " . number_format($count) . "回\n";
}
?>

解決方法のポイント:

  1. file_get_contents()ではなくfopen()fgets()を使用
  2. 1 行ずつ処理するため、ファイルサイズに関わらずメモリ使用量は一定
  3. 必要な統計情報だけをメモリに保持

実践例 4: API からの大量データ取得

外部 API から大量のデータを取得する場合も、メモリ効率を考慮する必要があります。

改善されたコード(ページネーション処理)

php<?php
// ✅ 良い例: ページネーションでAPIデータを取得
function fetchAllApiData($api_url, $api_key) {
    $all_results = [];
    $page = 1;
    $per_page = 100; // 1ページあたり100件

    while (true) {
        // ページごとにデータを取得
        $url = "{$api_url}?page={$page}&per_page={$per_page}";

        $options = [
            'http' => [
                'header' => "Authorization: Bearer {$api_key}\r\n"
            ]
        ];
        $context = stream_context_create($options);
        $response = file_get_contents($url, false, $context);

        if ($response === false) {
            break;
        }

        $data = json_decode($response, true);

        // データがなくなったらループを終了
        if (empty($data['items'])) {
            break;
        }

        // 取得したデータを処理(すべて保持せず処理する)
        foreach ($data['items'] as $item) {
            processApiItem($item); // 即座に処理
        }

        $page++;

        // レート制限に配慮して少し待機
        usleep(100000); // 0.1秒
    }
}

function processApiItem($item) {
    // データベースに保存、ファイルに出力など
    // 処理が終わったら$itemは自動的にメモリから解放される
}
?>

以下の表は、各実践例のメモリ使用量を比較したものです。

#処理内容改善前のメモリ改善後のメモリ削減率
110 万件の CSV エクスポート500MB〜1GB5MB〜10MB99%
21000 枚の画像リサイズ2GB〜5GB150MB〜200MB95%
32GB のログファイル解析2GB〜4GB5MB 以下99.7%
4API から 10 万件取得200MB〜500MB10MB〜20MB96%

これらの実践例から、適切なコーディング手法を使用することで、メモリ使用量を大幅に削減できることがわかりますね。

まとめ

PHP の「Allowed memory size exhausted」エラーは、メモリ制限を超えた際に発生する致命的なエラーです。本記事では、このエラーの原因から具体的な解決方法まで、実践的な内容を解説してきました。

エラーが発生した際は、まず原因を正確に特定することが重要です。単にメモリ制限を増やすだけでは根本的な解決にはならず、コードを最適化することが最も効果的な対処方法となります。

特に以下のポイントを意識することで、メモリ効率の良いコードを書くことができるでしょう。

データ処理の最適化: 大量のデータを扱う場合は、fetchAll()ではなくfetch()を使用して 1 行ずつ処理する方法に変更しましょう。ストリーミング処理やチャンク処理を活用することで、メモリ使用量を大幅に削減できます。

ファイル操作の改善: file_get_contents()ではなくfopen()fgets()を使用して、行ごとに読み込むようにしましょう。特に数百 MB 以上のファイルを扱う場合は、この方法が必須となりますね。

画像処理のメモリ管理: 画像処理後は必ずimagedestroy()でメモリを解放し、複数の画像を処理する場合は 1 枚ずつ処理するようにしましょう。高解像度画像は特にメモリを大量に消費するため、注意が必要です。

ジェネレータの活用: PHP 5.5 以降では、ジェネレータを使用することで、メモリ効率の良いコードを簡潔に書くことができます。大量のデータを扱う処理では、積極的に活用していきましょう。

メモリ監視の実装: 開発段階でメモリ使用状況を監視する仕組みを実装することで、問題を早期に発見できます。memory_get_usage()memory_get_peak_usage()を活用して、処理の各段階でのメモリ使用量を把握しましょう。

これらの知識を活用することで、安定した大規模データ処理を実現できるようになります。メモリエラーに悩まされることなく、効率的な PHP アプリケーションを開発していきましょう。

関連リンク