PHPのメモリ使用量の確認の方法について

採用はこちら

PHPのメモリ使用量を確認する方法は、大きく分けて次の4つです。

  • PHPコード内で現在のメモリ使用量を確認する
  • PHPのメモリ上限を確認する
  • OSやコンテナ側からPHPプロセス全体を確認する
  • プロファイラを使って原因箇所を分析する

PHP内部で取得できるメモリ使用量と、OSから見たプロセス全体のメモリ使用量は一致しません。

そのため、コードの改善を目的とする場合と、サーバーの容量設計を目的とする場合では、確認すべき指標が異なります。

目次

PHPコード内で現在のメモリ使用量を確認する

memory_get_usage()の基本的な使い方

PHPでは、memory_get_usage()を使用すると、スクリプト実行中のメモリ使用量を取得できます。

<?php

echo memory_get_usage() . PHP_EOL;

戻り値の単位はバイトです。

出力例は次のとおりです。

412560

そのままでは分かりにくいため、KiBやMiBへ変換して表示すると確認しやすくなります。

<?php

function formatBytes(int $bytes, int $precision = 2): string
{
    if ($bytes === 0) {
        return '0 B';
    }

    $sign = $bytes < 0 ? '-' : '';
    $absolute = abs($bytes);
    $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];

    $power = min(
        (int) floor(log($absolute, 1024)),
        count($units) - 1
    );

    return $sign
        . number_format(
            $absolute / (1024 ** $power),
            $precision
        )
        . ' '
        . $units[$power];
}

echo formatBytes(memory_get_usage()) . PHP_EOL;

出力例です。

402.89 KiB

1024単位で換算する場合、厳密にはKBやMBではなく、KiBやMiBと表記します。

一般的な管理画面ではMB表記もよく使われますが、技術資料ではKiBやMiBのほうが正確です。

memory_get_usage(false)とtrueの違い

memory_get_usage()には、真偽値の引数を指定できます。

<?php

echo 'PHP使用中: '
    . formatBytes(memory_get_usage(false))
    . PHP_EOL;

echo 'PHP確保済み: '
    . formatBytes(memory_get_usage(true))
    . PHP_EOL;

それぞれの意味は次のとおりです。

memory_get_usage(false)

falseを指定した場合、PHPのメモリ管理上、現在使用中とされているメモリ量を取得します。

memory_get_usage(false);

引数を省略した場合も同じです。

memory_get_usage();

この値は、OSから見た物理メモリ使用量ではありません。

「実メモリ使用量」や「RSS」とは異なるため、次のようなラベルで表示するのが適切です。

PHP管理上の使用量
PHP使用中メモリ

memory_get_usage(true)

trueを指定した場合、PHPのメモリマネージャがシステムから確保しているメモリ領域を取得します。

memory_get_usage(true);

PHPは必要になるたびに細かくメモリを取得するのではなく、ある程度まとめて確保します。

そのため、通常は次の関係になります。

memory_get_usage(false)
<=
memory_get_usage(true)

例えば、次のような結果になることがあります。

PHP使用中: 1.28 MiB
PHP確保済み: 2.00 MiB

コード内のデータ構造による増減を比較する場合はfalse、PHPが確保しているメモリ領域を確認する場合はtrueが参考になります。

ピークメモリ使用量を確認する

memory_get_peak_usage()の使い方

現在値だけでは、一時的に大量のメモリを使用した処理を見逃す可能性があります。

その場合は、memory_get_peak_usage()を使用します。

<?php

echo '現在使用量: '
    . formatBytes(memory_get_usage(false))
    . PHP_EOL;

echo 'ピーク使用量: '
    . formatBytes(memory_get_peak_usage(false))
    . PHP_EOL;

PHPがシステムから確保した領域のピークを確認する場合は、trueを指定します。

<?php

echo 'ピーク使用量: '
    . formatBytes(memory_get_peak_usage(false))
    . PHP_EOL;

echo 'ピーク確保量: '
    . formatBytes(memory_get_peak_usage(true))
    . PHP_EOL;

大量のCSV処理、JSONデコード、画像処理、データベースの全件取得などでは、終了時の現在値よりもピーク値のほうが重要です。

一時的に100MiB増加した後、処理終了時にほとんど解放された場合、終了時の使用量だけでは実際の負荷を把握できません。

PHP 8.2以降で区間ごとのピークを確認する

PHP 8.2以降では、memory_reset_peak_usage()を利用できます。

この関数を使うと、特定処理を開始する直前にピーク値をリセットできます。

<?php

$before = memory_get_usage(false);

if (function_exists('memory_reset_peak_usage')) {
    memory_reset_peak_usage();
}

runTargetProcess();

$after = memory_get_usage(false);
$peak = memory_get_peak_usage(false);

echo '開始時: ' . formatBytes($before) . PHP_EOL;
echo '終了時: ' . formatBytes($after) . PHP_EOL;
echo '前後差分: ' . formatBytes($after - $before) . PHP_EOL;
echo '区間ピーク: ' . formatBytes($peak) . PHP_EOL;

ただし、リセット後のピーク値は増加量ではなく、その時点で観測された絶対的な使用量です。

開始時からどれだけ増加したかを確認する場合は、次のように計算します。

$peakIncrease = max(0, $peak - $before);

echo '区間内ピーク増加量: '
    . formatBytes($peakIncrease)
    . PHP_EOL;

特定処理の前後でメモリ使用量を比較する

処理前後の差分を取得する

特定の処理によるメモリ使用量の変化を確認する場合は、処理前後の値を比較します。

<?php

$before = memory_get_usage(false);

$data = range(1, 100000);

$after = memory_get_usage(false);

echo '処理前: ' . formatBytes($before) . PHP_EOL;
echo '処理後: ' . formatBytes($after) . PHP_EOL;
echo '増減量: ' . formatBytes($after - $before) . PHP_EOL;

ここで確認できるのは、処理前後におけるPHPメモリ管理上の使用量の変化です。

「その処理が合計で確保したメモリ量」ではありません。

差分には、次の要素が影響します。

  • PHP内部で確保済み領域が再利用された
  • 一時変数が処理中に生成され、終了前に破棄された
  • ガベージコレクションが実行された
  • コピーオンライトが発生した
  • PHP内部のキャッシュが利用された
  • 拡張モジュールが独自にメモリを確保した

そのため、前後差分だけでなくピーク値も併せて確認することが重要です。

負の差分にも対応する

処理後にメモリ使用量が減ると、差分は負数になります。

フォーマット関数で負数を0 Bとして扱ってしまうと、正しい増減を確認できません。

次のように符号を保持する必要があります。

function formatBytes(int $bytes, int $precision = 2): string
{
    if ($bytes === 0) {
        return '0 B';
    }

    $sign = $bytes < 0 ? '-' : '';
    $absolute = abs($bytes);
    $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];

    $power = min(
        (int) floor(log($absolute, 1024)),
        count($units) - 1
    );

    return $sign
        . number_format(
            $absolute / (1024 ** $power),
            $precision
        )
        . ' '
        . $units[$power];
}

出力例です。

増減量: -2.34 MiB

unset()後にメモリ使用量が下がらない理由

PHPは確保済みメモリを再利用する

不要になった変数は、unset()で削除できます。

unset($data);

ただし、unset()を実行しても、memory_get_usage(true)がすぐに下がるとは限りません。

PHPは、一度システムから確保した領域をすぐにOSへ返さず、後続処理で再利用する場合があるためです。

<?php

echo '開始時: '
    . formatBytes(memory_get_usage(true))
    . PHP_EOL;

$data = range(1, 1000000);

echo '生成後: '
    . formatBytes(memory_get_usage(true))
    . PHP_EOL;

unset($data);

echo 'unset後: '
    . formatBytes(memory_get_usage(true))
    . PHP_EOL;

unset()後にmemory_get_usage(false)が減っても、memory_get_usage(true)は変わらないことがあります。

これは必ずしもメモリリークを意味しません。

PHPが確保済み領域を再利用可能な状態で保持している可能性があります。

他の変数が参照している場合は解放されない

同じデータを別の変数が参照している場合、片方をunset()しても値自体は解放されません。

<?php

$a = range(1, 100000);
$b = $a;

unset($a);

この場合、$bがデータを保持しているため、配列は解放されません。

PHPではコピーオンライトが使われるため、代入した時点ですぐに完全コピーされるとは限りませんが、少なくとも参照先のデータは残ります。

スクリプト終了時にメモリ使用量を記録する

register_shutdown_function()を使用する

スクリプト終了時にメモリ使用量を確認したい場合は、register_shutdown_function()を利用できます。

<?php

register_shutdown_function(function (): void {
    error_log(sprintf(
        'memory_usage=%d allocated=%d peak=%d allocated_peak=%d',
        memory_get_usage(false),
        memory_get_usage(true),
        memory_get_peak_usage(false),
        memory_get_peak_usage(true)
    ));
});

Webアプリケーションでは、画面へ直接表示するとHTMLやJSONレスポンスを壊す可能性があります。

本番環境では、echoではなくログへ出力するほうが安全です。

Fatal Errorだけを判定する

error_get_last()は、必ずしもFatal Errorだけを返すわけではありません。

最後に発生したWarningやNoticeが返る可能性もあります。

致命的なエラーだけを記録する場合は、エラー種別を確認します。

<?php

register_shutdown_function(function (): void {
    $error = error_get_last();

    if ($error === null) {
        return;
    }

    $fatalTypes = [
        E_ERROR,
        E_PARSE,
        E_CORE_ERROR,
        E_COMPILE_ERROR,
        E_USER_ERROR,
        E_RECOVERABLE_ERROR,
    ];

    if (!in_array($error['type'], $fatalTypes, true)) {
        return;
    }

    error_log(sprintf(
        'Fatal PHP error: type=%d message=%s file=%s line=%d peak=%d',
        $error['type'],
        $error['message'],
        $error['file'],
        $error['line'],
        memory_get_peak_usage(true)
    ));
});

メモリ不足時の緊急用領域を確保する

メモリを完全に使い切ると、エラーログを出力するためのメモリまで不足することがあります。

その対策として、あらかじめ小さな緊急用メモリを確保しておき、終了時に解放する方法があります。

<?php

$reservedMemory = str_repeat('x', 1024 * 1024);

register_shutdown_function(
    function () use (&$reservedMemory): void {
        $reservedMemory = null;

        $error = error_get_last();

        if ($error === null) {
            return;
        }

        error_log(sprintf(
            'PHP error: %s peak=%d',
            $error['message'],
            memory_get_peak_usage(true)
        ));
    }
);

この例では1MiBを予約しています。

ただし、次の点には注意が必要です。

  • 予約領域自体が通常時のメモリ使用量を増やす
  • 必ずログ出力に成功する保証はない
  • 複雑なログライブラリは追加メモリを必要とする
  • PHP-FPMではリクエストごとに予約領域を確保する

メモリ不足時のログは、できるだけ短い文字列をerror_log()で出力するのが安全です。

PHPのメモリ上限を確認する

ini_get()でmemory_limitを確認する

PHPが1スクリプトで使用できるメモリ上限は、memory_limitで設定されます。

コード内では次のように確認できます。

<?php

echo ini_get('memory_limit') . PHP_EOL;

出力例です。

128M

CLIから確認する場合は、次のコマンドを使用できます。

php -r "echo ini_get('memory_limit'), PHP_EOL;"

LinuxやmacOSでは、次の方法もあります。

php -i | grep memory_limit

Windowsでは次のように確認できます。

php -i | findstr memory_limit

php.iniの設定を確認する

php.iniでは、次のように設定します。

memory_limit = 128M

代表的な設定例です。

memory_limit = 256M

制限を設けない場合は、-1を指定します。

memory_limit = -1

ただし、本番環境で無制限にするのは通常おすすめできません。

メモリリークや想定外の巨大データによって、サーバーやコンテナ全体のメモリを使い切る可能性があるためです。

PHP 8.2以降ではini_parse_quantity()を使用する

ini_get('memory_limit')は、128Mなどの文字列を返します。

PHP 8.2以降では、ini_parse_quantity()を使ってバイト数へ変換できます。

<?php

$limitText = ini_get('memory_limit');

if ($limitText === '-1') {
    echo 'メモリ制限なし' . PHP_EOL;
} else {
    $limitBytes = ini_parse_quantity($limitText);

    echo formatBytes($limitBytes) . PHP_EOL;
}

PHP 8.1以前にも対応する場合は、独自の変換処理を用意します。

<?php

function parseMemoryLimit(string $value): int
{
    if ($value === '-1') {
        return -1;
    }

    if (function_exists('ini_parse_quantity')) {
        return ini_parse_quantity($value);
    }

    $value = trim($value);
    $unit = strtolower(substr($value, -1));
    $number = (float) $value;

    return match ($unit) {
        'g' => (int) ($number * 1024 ** 3),
        'm' => (int) ($number * 1024 ** 2),
        'k' => (int) ($number * 1024),
        default => (int) $number,
    };
}

memory_limitまでの残量計算に関する注意点

正確な残量は単純計算できない

次のような計算は、参考値としては利用できます。

$remaining = $limit - memory_get_usage(false);

ただし、これは「あと何バイト確実に確保できるか」を示すものではありません。

理由は次のとおりです。

  • PHP内部の管理用領域が必要になる
  • 次の処理が大きな連続領域を必要とすることがある
  • 拡張モジュールがPHPの計測外でメモリを使用する場合がある
  • OSやコンテナ側の上限が先に到達する可能性がある
  • 一時変数の生成によって急激にピークが増えることがある

そのため、「残りメモリ」と断定するのではなく、「memory_limitに対する参考使用率」として扱うのが適切です。

<?php

$limitText = ini_get('memory_limit');
$limit = parseMemoryLimit($limitText);

$usage = memory_get_usage(false);
$allocated = memory_get_usage(true);

echo 'PHP使用中: '
    . formatBytes($usage)
    . PHP_EOL;

echo 'PHP確保済み: '
    . formatBytes($allocated)
    . PHP_EOL;

if ($limit === -1) {
    echo 'memory_limit: 制限なし' . PHP_EOL;
} else {
    $percentage = ($usage / $limit) * 100;

    echo 'memory_limit: '
        . formatBytes($limit)
        . PHP_EOL;

    echo '参考使用率: '
        . number_format($percentage, 2)
        . '%'
        . PHP_EOL;
}

実行時にmemory_limitを変更する

ini_set()で一時的に変更する

スクリプト内で一時的にmemory_limitを変更できます。

<?php

$result = ini_set('memory_limit', '512M');

if ($result === false) {
    throw new RuntimeException(
        'memory_limitを変更できませんでした'
    );
}

ini_set()が成功すると、変更前の値を返します。

失敗するとfalseを返します。

変更は、そのスクリプトの実行中だけ有効です。

<?php

$oldLimit = ini_get('memory_limit');

$result = ini_set('memory_limit', '512M');

if ($result === false) {
    throw new RuntimeException(
        'memory_limitを変更できませんでした'
    );
}

echo '変更前: ' . $oldLimit . PHP_EOL;
echo '変更後: ' . ini_get('memory_limit') . PHP_EOL;

CLIコマンドだけ上限を変更する

特定のCLI処理だけ上限を変更する場合は、-dオプションを使用できます。

php -d memory_limit=512M command.php

Webアプリケーション全体の上限を引き上げず、重いバッチ処理だけ個別に調整できます。

ただし、上限を上げる前に、不要な一括取得や巨大配列がないか確認することが重要です。

読み込まれているphp.iniを確認する

CLIの設定ファイルを確認する

CLIで使用されている設定ファイルは、次のコマンドで確認できます。

php --ini

または、次のコマンドを使用します。

php -r "echo php_ini_loaded_file(), PHP_EOL;"

PHPコード内では次のように確認できます。

<?php

echo php_ini_loaded_file() ?: 'php.iniなし';

CLIとPHP-FPMでは設定が異なる場合がある

PHPでは、実行方法によって異なるphp.iniを使用することがあります。

例です。

/etc/php/8.3/cli/php.ini
/etc/php/8.3/fpm/php.ini
/etc/php/8.3/apache2/php.ini

CLI上で確認したmemory_limitと、NginxやApache経由で実行した際の値が異なることは珍しくありません。

Web経由の設定を確認する場合は、一時的に診断用スクリプトを作成します。

<?php

header('Content-Type: text/plain; charset=UTF-8');

echo 'SAPI: ' . PHP_SAPI . PHP_EOL;
echo 'php.ini: '
    . (php_ini_loaded_file() ?: 'none')
    . PHP_EOL;

echo 'memory_limit: '
    . ini_get('memory_limit')
    . PHP_EOL;

調査後は、必ず診断スクリプトを削除してください。

phpinfo()を使用する場合の注意点

phpinfo()でPHP設定を確認する

Web環境のPHP設定は、phpinfo()でも確認できます。

<?php

phpinfo();

ブラウザでページを開き、memory_limitを検索します。

ただし、phpinfo()には次のような情報が表示される可能性があります。

  • PHPのバージョン
  • サーバー内のパス
  • 読み込まれている拡張機能
  • 環境変数
  • 設定ファイルの場所
  • HTTPヘッダー
  • セッション設定
  • ビルド情報

本番環境で公開したままにするのは危険です。

必要な情報だけを表示する診断ページのほうが安全です。

<?php

header('Content-Type: application/json; charset=UTF-8');

echo json_encode([
    'sapi' => PHP_SAPI,
    'php_version' => PHP_VERSION,
    'ini_file' => php_ini_loaded_file(),
    'memory_limit' => ini_get('memory_limit'),
    'memory_usage' => memory_get_usage(false),
    'memory_allocated' => memory_get_usage(true),
    'memory_peak' => memory_get_peak_usage(false),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

このような診断ページも、認証やIP制限を行い、調査後は削除してください。

メモリ不足エラーの見方

Allowed memory size exhaustedの意味

PHPがmemory_limitを超えると、一般的に次のようなエラーが発生します。

Allowed memory size of 134217728 bytes exhausted
(tried to allocate 4096 bytes)

この場合、

134217728 bytes

は128MiBです。

tried to allocate 4096 bytes

は、最後に追加で4096バイトを確保しようとして失敗したことを示します。

これは、4096バイトの処理自体が問題だったという意味ではありません。

すでにメモリ上限付近まで使用しており、最後の4096バイトを確保できなかったという意味です。

エラーメッセージだけでは、どの処理が大量のメモリを使用したかは分かりません。

処理区間ごとのログやピーク値の計測が必要です。

error_log()でメモリ使用量を記録する

処理区間ごとにログを出す

Webアプリケーションでは、画面に出力するよりログへ記録する方法が一般的です。

<?php

error_log(sprintf(
    'usage=%d allocated=%d peak=%d',
    memory_get_usage(false),
    memory_get_usage(true),
    memory_get_peak_usage(false)
));

処理の前後で記録すると、どの区間で増加したか確認できます。

<?php

error_log(sprintf(
    '[before query] usage=%d allocated=%d',
    memory_get_usage(false),
    memory_get_usage(true)
));

$rows = fetchLargeData();

error_log(sprintf(
    '[after query] usage=%d allocated=%d peak=%d',
    memory_get_usage(false),
    memory_get_usage(true),
    memory_get_peak_usage(false)
));

環境変数の真偽値判定に注意する

環境変数は文字列として取得されることがあります。

次のコードでは、文字列"false"が真として判定される可能性があります。

if ($_ENV['APP_DEBUG'] ?? false) {
    // ログ出力
}

安全に判定する場合は、FILTER_VALIDATE_BOOLを使用します。

<?php

$debug = filter_var(
    $_ENV['APP_DEBUG'] ?? false,
    FILTER_VALIDATE_BOOL
);

if ($debug) {
    error_log(sprintf(
        'memory=%d',
        memory_get_usage(true)
    ));
}

Laravelでは、環境変数を直接参照するより、設定値を利用します。

if (config('app.debug')) {
    Log::debug('memory usage', [
        'usage' => memory_get_usage(false),
        'allocated' => memory_get_usage(true),
        'peak' => memory_get_peak_usage(false),
    ]);
}

Laravelでメモリ使用量を確認する

ログへ現在値とピーク値を記録する

Laravelでも、PHP標準関数をそのまま使用できます。

use Illuminate\Support\Facades\Log;

Log::info('memory usage', [
    'usage_bytes' => memory_get_usage(false),
    'allocated_bytes' => memory_get_usage(true),
    'peak_bytes' => memory_get_peak_usage(false),
    'allocated_peak_bytes' => memory_get_peak_usage(true),
]);

MiB単位で表示する場合は次のようにします。

use Illuminate\Support\Facades\Log;

Log::info('memory usage', [
    'usage_mib' => round(
        memory_get_usage(false) / 1024 / 1024,
        2
    ),
    'allocated_mib' => round(
        memory_get_usage(true) / 1024 / 1024,
        2
    ),
    'peak_mib' => round(
        memory_get_peak_usage(false) / 1024 / 1024,
        2
    ),
]);

ミドルウェアでリクエスト単位に記録する

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogMemoryUsage
{
    public function handle(
        Request $request,
        Closure $next
    ): Response {
        $before = memory_get_usage(false);

        if (function_exists('memory_reset_peak_usage')) {
            memory_reset_peak_usage();
        }

        /** @var Response $response */
        $response = $next($request);

        $after = memory_get_usage(false);

        Log::info('request memory usage', [
            'method' => $request->method(),
            'path' => $request->path(),
            'before_mib' => round(
                $before / 1024 / 1024,
                2
            ),
            'after_mib' => round(
                $after / 1024 / 1024,
                2
            ),
            'delta_mib' => round(
                ($after - $before) / 1024 / 1024,
                2
            ),
            'peak_mib' => round(
                memory_get_peak_usage(false)
                / 1024
                / 1024,
                2
            ),
        ]);

        return $response;
    }
}

通常のPHP-FPMでは、リクエスト単位の計測に利用できます。

ただし、次のような常駐型環境では注意が必要です。

  • Laravel Octane
  • Swoole
  • RoadRunner
  • 長時間稼働するQueue Worker
  • 常駐CLIプロセス

同じPHPプロセスが複数のリクエストやジョブを処理するため、ピーク値が今回の処理だけを表しているとは限りません。

SymfonyとDoctrineでメモリ使用量を確認する

Symfonyのロガーへ記録する

Symfonyでも標準関数を利用できます。

$this->logger->info('memory usage', [
    'usage' => memory_get_usage(false),
    'allocated' => memory_get_usage(true),
    'peak' => memory_get_peak_usage(false),
]);

Doctrineの管理オブジェクトを定期的に解放する

Doctrineで大量のエンティティを処理すると、EntityManagerが管理するオブジェクトが増え続ける場合があります。

一定件数ごとにflush()clear()を実行します。

foreach ($items as $index => $item) {
    // 更新処理

    if (($index + 1) % 100 === 0) {
        $entityManager->flush();
        $entityManager->clear();

        $this->logger->info('batch memory', [
            'processed' => $index + 1,
            'usage' => memory_get_usage(false),
            'allocated' => memory_get_usage(true),
        ]);
    }
}

ただし、clear()すると、EntityManagerが管理していたエンティティはデタッチされます。

後続処理で同じエンティティを利用する場合は、再取得が必要になることがあります。

WordPressでメモリ使用量を確認する

WordPressのsize_format()を使用する

WordPressでは、size_format()を利用できます。

error_log(
    'Memory: '
    . size_format(memory_get_usage(true))
);

error_log(
    'Peak Memory: '
    . size_format(memory_get_peak_usage(true))
);

WordPressのメモリ定数を確認する

WordPressでは、次の定数が使用される場合があります。

define('WP_MEMORY_LIMIT', '256M');

より多くのメモリを必要とする特定処理では、WP_MAX_MEMORY_LIMITが利用されます。

define('WP_MAX_MEMORY_LIMIT', '512M');

ただし、WordPress側で大きな値を設定しても、サーバーやホスティング環境のPHP設定を必ず超えられるわけではありません。

実際の値も確認します。

error_log(
    'PHP memory_limit='
    . ini_get('memory_limit')
);

データベース処理でメモリを抑える

fetchAll()による全件取得を避ける

大量データをfetchAll()で取得すると、全レコードがPHP配列としてメモリに保持されます。

$rows = $pdo->query($sql)->fetchAll();

大量データでは、1件ずつ取得します。

$stmt = $pdo->query($sql);

while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    processRow($row);
}

ただし、MySQLのPDOでは、結果セットがドライバ側でバッファリングされる場合があります。

その場合、1件ずつfetch()しても、結果全体がドライバ内部に保持される可能性があります。

MySQLで非バッファクエリを使用する

本当に逐次取得したい場合は、非バッファモードを検討します。

<?php

$pdo = new PDO(
    $dsn,
    $username,
    $password,
    [
        PDO::ATTR_ERRMODE
            => PDO::ERRMODE_EXCEPTION,
        PDO::MYSQL_ATTR_USE_BUFFERED_QUERY
            => false,
    ]
);

$stmt = $pdo->query($sql);

while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    processRow($row);
}

非バッファクエリには制約があります。

結果セットを最後まで読み終える前に、同じ接続で別のクエリを実行できない場合があります。

使用するデータベースやドライバの仕様を確認したうえで利用してください。

CSVやテキストファイルを逐次処理する

file_get_contents()とexplode()を避ける

次のコードは、ファイル全体と行配列の両方をメモリへ保持します。

$contents = file_get_contents($file);
$lines = explode("\n", $contents);

大きなファイルでは、1行ずつ処理します。

<?php

$handle = fopen($file, 'rb');

if ($handle === false) {
    throw new RuntimeException(
        'ファイルを開けませんでした'
    );
}

try {
    while (($row = fgetcsv($handle)) !== false) {
        processRow($row);
    }
} finally {
    fclose($handle);
}

ジェネレーターを使用する

ジェネレーターを使うと、全件を配列へ格納せず、1件ずつ返せます。

<?php

function readLines(string $file): Generator
{
    $handle = fopen($file, 'rb');

    if ($handle === false) {
        throw new RuntimeException(
            'ファイルを開けませんでした'
        );
    }

    try {
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }
    } finally {
        fclose($handle);
    }
}

foreach (readLines('large.txt') as $line) {
    processLine($line);
}

ただし、呼び出し側でジェネレーターを配列へ変換すると、結局すべてのデータをメモリに保持します。

$allLines = iterator_to_array(
    readLines('large.txt')
);

ジェネレーターを使う場合は、最後まで逐次処理することが重要です。

巨大JSONを扱う場合の注意点

JSONファイルサイズとメモリ使用量は一致しない

次の処理では、JSON文字列とデコード後のPHP配列が同時にメモリへ存在します。

$data = json_decode($largeJson, true);

概念的には次の領域が必要です。

JSON文字列
+
デコード済みPHP配列
+
デコード中の一時領域

PHPの連想配列は管理用のオーバーヘッドが大きいため、100MiBのJSONファイルが100MiBのメモリだけで処理できるとは限りません。

$json = file_get_contents($file);

$data = json_decode(
    $json,
    true,
    512,
    JSON_THROW_ON_ERROR
);

unset($json);

unset($json)は、デコード後のメモリを減らすためには有効です。

ただし、デコード中のピークメモリを減らすことはできません。

デコード完了までは、元のJSON文字列とデコード結果の両方が必要だからです。

巨大JSONでは、次の方法を検討します。

  • ストリーミングJSONパーサーを利用する
  • NDJSON形式へ変更する
  • APIをページングする
  • 不要なフィールドを返さない
  • データを複数ファイルに分割する

画像処理のメモリ使用量に注意する

圧縮ファイルサイズでは判断できない

JPEGやPNGのファイルサイズが小さくても、メモリ上では画像が展開されます。

概算は次のとおりです。

横幅 × 高さ × チャンネル数

例えば、6000×4000ピクセルをRGBAで保持するとします。

6000 × 4000 × 4
= 96,000,000 bytes
≈ 91.6 MiB

実際には、さらに次の領域が必要になる可能性があります。

  • 画像ライブラリの管理領域
  • デコード用バッファ
  • 色空間変換用の一時領域
  • リサイズ前後の画像
  • 回転処理時の複製
  • サムネイル生成用画像
  • 行ごとのアラインメント

そのため、実際のピークメモリは単純計算の2倍以上になる場合があります。

画像処理では、ファイルサイズではなく、画像の幅と高さを確認することが重要です。

PHPのガベージコレクションを確認する

gc_status()で状態を確認する

ガベージコレクションの状態は、次の関数で確認できます。

<?php

var_dump(gc_enabled());
var_dump(gc_status());

gc_collect_cycles()は循環参照向け

gc_collect_cycles()は、主に循環参照によって通常の参照カウントだけでは解放できないデータを回収します。

<?php

$collected = gc_collect_cycles();

echo '回収数: '
    . $collected
    . PHP_EOL;

通常の変数をunset()した後に毎回呼び出せば、すべてのメモリが解放されるという関数ではありません。

次のように、ループのたびに実行するのは性能面で不利になる可能性があります。

foreach ($items as $item) {
    process($item);
    gc_collect_cycles();
}

必要な場合は、一定件数ごとに実行し、効果を計測します。

foreach ($items as $index => $item) {
    process($item);

    if (($index + 1) % 1000 === 0) {
        $collected = gc_collect_cycles();

        error_log(sprintf(
            'processed=%d collected=%d memory=%d',
            $index + 1,
            $collected,
            memory_get_usage(false)
        ));
    }
}

CLI処理のメモリをOS側から確認する

/usr/bin/timeを使用する

Linuxでは、/usr/bin/timeを使うと、CLI処理全体の最大メモリを確認できます。

/usr/bin/time -v php script.php

重要な項目は次のとおりです。

Maximum resident set size

これは、プロセスの最大RSSを示します。

簡易形式では次のように確認できます。

/usr/bin/time \
  -f "max_rss=%M KB" \
  php script.php

シェル組み込みのtimeでは表示項目が異なる場合があるため、/usr/bin/timeを明示するのが確実です。

psやtopでPHPプロセスを確認する

psでRSSを確認する

稼働中のPHPプロセスは、次のように確認できます。

ps aux | grep php

より見やすく表示する場合は次のコマンドを使用します。

ps -eo pid,ppid,%mem,rss,vsz,cmd \
  | grep php

主な項目は次のとおりです。

RSS

物理メモリ上に常駐しているメモリ量です。

VSZ

プロセスが使用している仮想アドレス空間のサイズです。

%MEM

システム全体の物理メモリに対する割合です。

サーバーの実際の負荷を確認する場合は、VSZよりRSSを重視します。

topやhtopでリアルタイム監視する

リアルタイムで確認する場合は、tophtopを使用します。

top

PHP-FPMプロセスに絞る場合は、環境によって次のように確認できます。

pgrep -a php

htopでは、プロセス名でphp-fpmを検索すると確認しやすくなります。

主に確認する項目は次のとおりです。

  • RES
  • VIRT
  • SHR
  • MEM%

PHP内部の値とOSのRSSが異なる理由

計測対象が異なる

次の値は一致しないことがあります。

memory_get_usage(true);
ps -o rss

memory_get_usage()は、主にPHPのメモリ管理下にある領域を対象とします。

一方、OSのRSSには次のような領域が含まれる可能性があります。

  • PHP実行バイナリ
  • PHP拡張
  • 共有ライブラリ
  • ネイティブライブラリが確保した領域
  • スタック
  • メモリマップ
  • データベースクライアント
  • 画像処理ライブラリ
  • Xdebug
  • APMエージェント
  • JIT関連領域

そのため、memory_get_usage(true)が100MiBでも、RSSが150MiB以上になることがあります。

コード内の比較にはPHP標準関数を利用し、サーバーの容量設計にはRSSやPSSを利用するのが基本です。

PHP-FPM全体のメモリを確認する

PHP-FPMワーカーのRSSを確認する

PHP-FPMでは、複数のワーカープロセスが動作します。

個別のRSSは次のように確認できます。

ps -C php-fpm \
  -o pid,rss,%mem,cmd

プロセス名が異なる場合は、次のコマンドで確認します。

ps aux | grep '[p]hp-fpm'

RSSの単純合計は次のように取得できます。

ps --no-headers \
  -o rss \
  -C php-fpm \
  | awk '
    {
        sum += $1
    }
    END {
        print sum / 1024 " MiB"
    }
'

RSSの単純合計は過大評価になることがある

複数のPHP-FPMプロセスでは、共有ライブラリや共有メモリが重複してRSSへ計上される場合があります。

そのため、各プロセスのRSSを単純に合計すると、実際の物理メモリ使用量より大きく見える可能性があります。

より精密に確認する場合は、PSSが参考になります。

Linuxでは、smemを利用できます。

smem -P 'php-fpm'

主な指標は次のとおりです。

  • RSS:プロセスから見える常駐メモリ
  • PSS:共有メモリをプロセス数で按分した値
  • USS:そのプロセス固有のメモリ

pm.max_childrenを決める場合の注意点

平均メモリだけで計算しない

PHP-FPMのpm.max_childrenは、概算として次の式で検討できます。

PHP-FPMへ割り当て可能なメモリ
÷
ワーカー1つあたりのメモリ使用量

ただし、平均値だけで判断するのは危険です。

平均60MiBでも、重いリクエストで200MiB使用する場合、同時実行時にOOMが発生する可能性があります。

次の値を参考にします。

  • 高負荷時のRSSまたはPSS
  • 重いエンドポイントのピーク値
  • 95パーセンタイルなどの高水準値
  • バッチ処理や画像処理時の最大値
  • APMで観測した実測値

また、次のメモリも確保しておく必要があります。

  • OS
  • NginxやApache
  • データベース
  • Redis
  • OPcache
  • 監視エージェント
  • ファイルキャッシュ
  • PHP-FPMマスタープロセス

Docker環境でメモリを確認する

docker statsを使用する

Dockerコンテナのメモリ使用量は、次のコマンドで確認できます。

docker stats

特定コンテナだけ確認する場合です。

docker stats php-container

PHPの設定は次のように確認できます。

docker exec php-container \
  php -i \
  | grep memory_limit

コンテナのメモリ上限を確認する

Docker側のメモリ上限は次のように確認できます。

docker inspect \
  --format='{{.HostConfig.Memory}}' \
  php-container

戻り値の単位はバイトです。

0の場合、Docker側で明示的なメモリ上限が設定されていないことを示すのが一般的です。

PHPのmemory_limitとは別に考える

次のような構成を考えます。

PHP memory_limit = 512M
Docker memory limit = 256M

この場合、PHPは512MiBまで使用できる設定でも、コンテナが先に256MiBへ到達する可能性があります。

その場合、PHPの次のエラーが出ないことがあります。

Allowed memory size exhausted

代わりに、コンテナがOOM Killされる可能性があります。

確認例です。

docker inspect \
  --format='{{.State.OOMKilled}} {{.State.ExitCode}}' \
  php-container

Kubernetes環境でメモリを確認する

kubectl topを使用する

Podのメモリ使用量は、次のコマンドで確認できます。

kubectl top pod

特定Namespaceの場合です。

kubectl top pod \
  -n production

コンテナ単位で確認する場合です。

kubectl top pod pod-name \
  --containers \
  -n production

requestsとlimitsを確認する

Podの設定は次のコマンドで確認できます。

kubectl describe pod pod-name \
  -n production

一般的に、メモリのrequestsとlimitsはコンテナ単位で指定します。

resources:
  requests:
    memory: "256Mi"
  limits:
    memory: "512Mi"

確認時は、次の値を別々に考えます。

  • PHPのmemory_limit
  • 各コンテナのmemory limit
  • Pod内の全コンテナの合計使用量
  • Node全体の空きメモリ

コンテナ上限を超えると、PHPのFatal ErrorではなくOOMKilledになる可能性があります。

Xdebugやプロファイラを利用する

Xdebugで関数単位の調査を行う

Xdebugのトレースを利用すると、関数呼び出しや実行時間、設定によってはメモリ使用量を確認できます。

ただし、Xdebug自体にもオーバーヘッドがあります。

Xdebugを有効にすると、通常実行より実行時間やメモリ使用量が増える場合があります。

そのため、次のように使い分けます。

  • 開発環境で利用する
  • 本番環境で常時有効にしない
  • 対象リクエストだけで有効にする
  • Xdebug有効時と無効時を比較する

プロファイラごとの役割を理解する

目的によって利用するツールが異なります。

PHP標準関数

処理区間ごとのメモリ変化を確認します。

OSやコンテナの監視

プロセス全体やコンテナ全体のメモリを確認します。

Xdebug

関数呼び出しや実行経路を調査します。

APMやBlackfire

実行時間、呼び出し回数、ボトルネック、変更前後の差を分析します。

ヒープ分析ツール

どのオブジェクトが保持され続けているかを詳細に調査します。

1つのツールだけですべての原因を特定できるわけではありません。

長時間動作する処理でメモリ増加を確認する

一定件数ごとに記録する

大量データを処理するバッチでは、一定件数ごとにメモリ使用量を記録します。

<?php

foreach ($items as $index => $item) {
    process($item);

    if (($index + 1) % 1000 === 0) {
        printf(
            "件数=%d 使用中=%s 確保済み=%s ピーク=%s\n",
            $index + 1,
            formatBytes(memory_get_usage(false)),
            formatBytes(memory_get_usage(true)),
            formatBytes(memory_get_peak_usage(false))
        );
    }
}

正常な逐次処理であれば、初期段階で少し増えた後、ある程度横ばいになることが一般的です。

次のように増え続ける場合は、データを保持し続けている可能性があります。

1,000件: 40 MiB
2,000件: 65 MiB
3,000件: 90 MiB
4,000件: 115 MiB

例えば、次のコードでは全結果を保持します。

$results = [];

foreach ($items as $item) {
    $results[] = process($item);
}

全結果を保持する必要がない場合は、その場で保存や出力を行い、不要になったデータを残さないようにします。

実務向けのメモリ計測クラス

開始時と前回計測時からの差分を記録する

複数箇所のメモリ使用量をまとめて計測する場合は、簡単な計測クラスを用意すると便利です。

<?php

final class MemoryProfiler
{
    private int $startUsage;
    private int $previousUsage;
    private float $startTime;

    /** @var list<array<string, int|float|string>> */
    private array $points = [];

    public function __construct()
    {
        $this->startUsage = memory_get_usage(false);
        $this->previousUsage = $this->startUsage;
        $this->startTime = microtime(true);
    }

    public function mark(string $label): void
    {
        $usage = memory_get_usage(false);
        $allocated = memory_get_usage(true);

        $this->points[] = [
            'label' => $label,
            'usage' => $usage,
            'allocated' => $allocated,
            'from_start' => $usage - $this->startUsage,
            'from_previous' => $usage
                - $this->previousUsage,
            'peak' => memory_get_peak_usage(false),
            'allocated_peak'
                => memory_get_peak_usage(true),
            'elapsed_seconds'
                => microtime(true) - $this->startTime,
        ];

        $this->previousUsage = $usage;
    }

    public function printReport(): void
    {
        foreach ($this->points as $point) {
            printf(
                "%s | usage=%s | allocated=%s"
                . " | start_delta=%s"
                . " | previous_delta=%s"
                . " | peak=%s"
                . " | elapsed=%.4fs\n",
                $point['label'],
                self::formatBytes(
                    (int) $point['usage']
                ),
                self::formatBytes(
                    (int) $point['allocated']
                ),
                self::formatBytes(
                    (int) $point['from_start']
                ),
                self::formatBytes(
                    (int) $point['from_previous']
                ),
                self::formatBytes(
                    (int) $point['peak']
                ),
                $point['elapsed_seconds']
            );
        }
    }

    private static function formatBytes(
        int $bytes,
        int $precision = 2
    ): string {
        if ($bytes === 0) {
            return '0 B';
        }

        $sign = $bytes < 0 ? '-' : '';
        $absolute = abs($bytes);
        $units = [
            'B',
            'KiB',
            'MiB',
            'GiB',
            'TiB',
        ];

        $power = min(
            (int) floor(
                log($absolute, 1024)
            ),
            count($units) - 1
        );

        return $sign
            . number_format(
                $absolute / (1024 ** $power),
                $precision
            )
            . ' '
            . $units[$power];
    }
}

利用例です。

<?php

$profiler = new MemoryProfiler();

$profiler->mark('start');

$data = range(1, 100000);

$profiler->mark('after range');

$mapped = array_map(
    static fn (int $value): int => $value * 2,
    $data
);

$profiler->mark('after map');

unset($data);

$profiler->mark('after unset');

$profiler->printReport();

このクラスでは、次の値を確認できます。

  • 現在のPHP使用量
  • PHP確保済み領域
  • 開始時からの増減
  • 前回計測地点からの増減
  • ピーク使用量
  • 経過時間

目的別に確認方法を使い分ける

特定処理の前後を比較したい場合

次の関数を使用します。

memory_get_usage(false);
memory_get_peak_usage(false);

PHP 8.2以降では、対象処理の直前に次の関数を使用できます。

memory_reset_peak_usage();

PHPが確保した領域を確認したい場合

次の関数を使用します。

memory_get_usage(true);
memory_get_peak_usage(true);

PHPのメモリ上限を確認したい場合

次の関数を使用します。

ini_get('memory_limit');

PHP 8.2以降でバイト数へ変換する場合は、次の関数を使用します。

ini_parse_quantity(
    ini_get('memory_limit')
);

CLI処理全体の最大メモリを確認したい場合

Linuxでは次のコマンドを使用します。

/usr/bin/time -v php script.php

PHP-FPMのサーバー負荷を確認したい場合

次の指標を確認します。

  • RSS
  • PSS
  • ワーカー数
  • 重いリクエストのピーク値
  • pm.max_children
  • サーバー全体の空きメモリ

DockerやKubernetesで確認したい場合

次の上限を分けて確認します。

  • PHPのmemory_limit
  • コンテナのmemory limit
  • Podやコンテナの実使用量
  • ホストやNodeの空きメモリ

PHPのメモリ調査で重要なポイント

PHPのメモリ使用量を調査するときは、1つの数値だけで判断しないことが重要です。

特に、次の4つは別の指標です。

memory_get_usage(false)

PHPのメモリ管理上、現在使用中の領域です。

memory_get_usage(true)

PHPがシステムから確保している領域です。

memory_limit

PHPスクリプトに設定されたメモリ上限です。

OSのRSSやPSS

PHP本体や拡張モジュールなどを含む、プロセス全体のメモリ使用量です。

コードの改善を目的とする場合は、処理前後のmemory_get_usage(false)とピーク値を確認します。

サーバーの容量設計を目的とする場合は、PHP内部の数値だけでなく、RSS、PSS、PHP-FPMワーカー数、コンテナ上限、同時実行数も確認する必要があります。

メモリ不足が発生した場合は、単純にmemory_limitを引き上げるだけでなく、次の点を確認してください。

  • データベースを全件取得していないか
  • 巨大な配列を複数保持していないか
  • JSONやCSVを一括で読み込んでいないか
  • ORMが大量のオブジェクトを保持していないか
  • ジェネレーターや分割処理へ変更できないか
  • 画像解像度が必要以上に大きくないか
  • 長時間プロセスで参照が残り続けていないか
  • DockerやKubernetes側の上限が先に到達していないか

PHP内部の計測値とOS側の計測値を使い分けることで、メモリ使用量の原因をより正確に特定できます。

以上、PHPのメモリ使用量の確認の方法についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次