メモリ管理とガベージコレクション
zval、参照カウント、GCの仕組みを確認します。
「メモリ管理とガベージコレクション」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
PHPのメモリ管理
PHPの値はzvalに格納され、主に参照カウントで管理されます。参照カウントで回収できないものを捕捉するために、循環参照コレクタも使用されます。これを理解すると、長時間稼働するワーカーでのメモリリーク、コピーのコスト、そしてgc_collect_cycles()が必要になるタイミングが分かります。
zval
zvalは、PHPのあらゆる値を格納するコンテナです。型タグと、値を格納する共用体で構成されます。スカラー値(int、float、bool)はzval内に値として格納され、参照カウントの対象にはなりません。複合型(string、array、object)は、ヒープ上の参照カウントされる構造体を指します。
<?php
// debug_zval_refcount-style inspection via gc + count
$arr = [1, 2, 3];
$copy = $arr; // refcount of the array bumps, no data copy yet (CoW)
// Modifying triggers copy-on-write separation
$copy[] = 4;
var_dump($arr); // still [1,2,3]
var_dump($copy); // [1,2,3,4]
?>参照カウント
参照カウントされる各値は、何個のzvalから参照されているかを追跡します。代入するとカウントが増え、unsetするかスコープを抜けると減ります。カウントがゼロになると、GCパスを待つことなくメモリが直ちに解放されます。PHPのメモリの大部分は、この方法で回収されます。
<?php
$a = str_repeat('x', 1000); // refcount 1 on the string
$b = $a; // refcount 2
unset($a); // refcount 1
unset($b); // refcount 0 -> freed instantly
echo "Refcounting frees deterministically\n";
?>コピーオンライト
代入してもデータはコピーされません。データを共有し、参照カウントを増やします(コピーオンライト)。実際に複製されるのは、共有された値に対して最初に書き込みを行ったときだけです。そのため、大きな配列も変更するまでは低コストで渡せます。
<?php
$big = range(1, 100000);
$shared = $big; // O(1): shared, refcount++
// Reads stay shared and cheap
echo count($shared), PHP_EOL;
// First write separates (copies) — now it costs O(n)
$shared[0] = -1;
echo $big[0], ' vs ', $shared[0], PHP_EOL; // 1 vs -1
?>問題:循環参照
参照カウントだけでは循環参照を解放できません。オブジェクトAがBへの参照を保持し、BがAへの参照を保持していると、外部変数をすべてunsetした後も両者のカウントはゼロになりません。そのため、循環参照コレクタが実行されるまでメモリリークが発生します。
<?php
class Node { public ?Node $ref = null; }
$a = new Node();
$b = new Node();
$a->ref = $b; // a -> b
$b->ref = $a; // b -> a (cycle!)
unset($a, $b); // external refs gone, but internal cycle keeps refcount > 0
// The objects are unreachable but NOT yet freed by refcounting alone.
echo gc_collect_cycles() . " cycles collected\n";
?>循環参照コレクタ
PHPのGCは、ルートに対する同期的なマークアンドスイープを実装しています。循環参照の候補となるルート(参照カウントが減少したもののゼロにはならなかった値)はバッファリングされます。ルートバッファがいっぱいになると(デフォルトは10,000)、GCが実行されます。GCは減算をシミュレートし、本当に到達不能なグループを見つけて解放します。
<?php
// GC status exposes the root buffer and run stats
for ($i = 0; $i < 3; $i++) {
$a = new stdClass();
$b = new stdClass();
$a->b = $b; $b->a = $a; // create a cycle
unset($a, $b); // becomes a GC root
}
print_r(gc_status());
?>GCの制御
コレクタは制御できます。gc_disable()/gc_enable()で有効・無効を切り替え、gc_collect_cycles()でパスを強制実行し、解放した数を返します。スループットが重要なバッチジョブでは、タイトな処理中にGCを無効にし、最後に1回だけ収集することがあります。
<?php
gc_disable(); // no automatic cycle collection
// ... heavy phase that creates and drops many cycles ...
for ($i = 0; $i < 1000; $i++) {
$x = new stdClass(); $y = new stdClass();
$x->y = $y; $y->x = $x; unset($x, $y);
}
$freed = gc_collect_cycles(); // reclaim all at once
gc_enable();
echo "Freed $freed objects\n";
?>長時間稼働するワーカー
CLIデーモン、キューコンシューマ、Swoole/RoadRunnerワーカーでは、メモリはリクエスト終了処理によって回収されません。プロセスが数時間にわたって存続するためです。無制限に増加する静的キャッシュ、蓄積する循環参照、クローズされていないリソースに注意してください。memory_get_usage(true)で監視し、ワーカーを定期的に再起動してください。
<?php
$peakStart = memory_get_usage(true);
// Simulate processing a batch of jobs
for ($job = 0; $job < 5; $job++) {
$payload = range(0, 10000);
// ... process ...
unset($payload);
}
printf("start: %d KB, peak: %d KB, now: %d KB\n",
$peakStart >> 10,
memory_get_peak_usage(true) >> 10,
memory_get_usage(true) >> 10
);
?>WeakReferenceとWeakMap
オブジェクトを生存させ続けることなく、オブジェクトにデータをキャッシュまたは関連付けるには、WeakReference(8.0)とWeakMap(8.0)を使用します。WeakMapのエントリはキーオブジェクトの参照カウントを増やさないため、オブジェクトが回収されると、そのエントリも自動的に削除されます。リークを起こしてはいけないメタデータ用の補助テーブルに最適です。
<?php
$map = new WeakMap();
$obj = new stdClass();
$map[$obj] = 'metadata'; // does NOT keep $obj alive
echo count($map) . PHP_EOL; // 1
unset($obj); // object collectible -> entry removed
echo count($map) . PHP_EOL; // 0
?>インターン文字列と不変データ
ソースコード内のリテラル文字列はインターン化されます。つまり、1度だけ格納され、共有され、リクエスト中に参照カウントも解放もされません。OPcacheはインターン文字列と不変の(キャッシュされた)op_arraysをリクエスト間で共有メモリに移します。そのため、文字列リテラルのコピーは実質的に無料です。
メモリ制限とアロケータ
PHPは独自のemallocアロケータ(Zend Memory Manager)をシステムアロケータの上に構築して使用し、memory_limitに対するリクエストごとの使用量を追跡します。制限に達すると致命的エラーが発生します。memory_get_usage(true)のtrueは、スクリプトが現在保持している量だけでなく、OSから実際に割り当てられたブロックを報告します。
<?php
echo 'limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'emalloc usage: ' . memory_get_usage() . " bytes\n"; // ZendMM tracked
echo 'real usage: ' . memory_get_usage(true) . " bytes\n"; // from OS
?>確認問題
参照カウントだけでは、一部のオブジェクトがリークするのはなぜですか?
まとめ
PHPは値をzvalに格納します。スカラー値は値として扱われ、複合型はヒープに割り当てられ、コピーオンライトを伴う参照カウントで管理されます。参照カウントはカウントがゼロになった時点で確実に解放しますが、循環参照は回収できません。そのため、ルートバッファがいっぱいになったとき、またはgc_collect_cycles()によって、同期的なマークアンドスイープコレクタが実行されます。所有権を持たない関連付けにはWeakMap/WeakReferenceを使用し、長時間稼働するワーカーの無制限な増加に注意してください。
よくある質問
「メモリ管理とガベージコレクション」レッスンは無料ですか?
はい。「メモリ管理とガベージコレクション」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「メモリ管理とガベージコレクション」で何を学びますか?
zval、参照カウント、GCの仕組みを確認します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「メモリ管理とガベージコレクション」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Zend Engineの仕組み
- メモリ管理とガベージコレクション
- OPcacheとJITコンパイル
- Cで基本的なPHP拡張機能を書く