unsetでメモリは即解放される、という思い込みを解く

unsetとは、変数名と値との結びつきを外す構文で、メモリを直接解放する命令ではありません。値が実際に回収されるのは、その値を指す参照カウントがゼロになったときです。

unsetは「変数を消す」であって「メモリを消す」ではない

大きな配列を処理したあとに unset($data) と書いて「よし、メモリが空いた」と安心してしまうことがありますが、これは半分正しくて半分間違いだと思います。unset がやるのは、その変数名と値との結びつきを外すことです。値そのものがメモリから消えるかどうかは、その値をほかに誰かが参照しているかどうか次第なんですね。

$a = ['大きな', '配列'];
$b = $a; // この時点で値の参照カウントは増える(実際はコピーオンライト)

unset($a);
// $a は消えたが、値は $b がまだ握っているので解放されない

参照カウントの話

PHPの値には内部的に参照カウントがあって、その値を指している変数の数を数えています。unset で結びつきが外れるとカウントが1減り、ゼロになったところで初めてメモリが回収される、というのが基本の仕組みです。だから & で参照を作っていたり、オブジェクトを複数箇所で持っていたりすると、unset しても即解放とはいきません。

$obj = new stdClass();
$holder = ['ref' => $obj];

unset($obj);
// $holder['ref'] がまだ握っているので、この stdClass は生きている

「メモリが減らない」と悩むときは、たいてい思わぬところで参照が残っています。クロージャがスコープの変数を握っていた、というのもよくあるパターンですね。

循環参照はGC任せになる

オブジェクト同士がお互いを参照し合っていると、参照カウントは片方を unset してもゼロになりません。こういう循環はPHPの循環参照ガベージコレクタが後から回収してくれますが、タイミングは即時ではありません。手動で gc_collect_cycles() を呼ぶ手もありますが、常用するものではないと思います。

$parent = new stdClass();
$child = new stdClass();
$parent->child = $child;
$child->parent = $parent; // 循環

unset($parent, $child);
// カウントは1ずつ残る。GCが回るまで生存

まとめ

unset は「解放命令」ではなく「切り離し」だと捉えておくと、メモリ周りの挙動に納得がいくと思います。本当に使い終わった大きなデータをこまめに切り離すのは意味がありますが、それで必ず即座に空くわけではない、という距離感を持っておくのがちょうどいい気がします。参照の残りやすさを疑うほうが、実務では役に立つ場面が多いですね。

よくある質問

Q. unset したのにメモリが減りません。
A. その値をほかの変数やクロージャがまだ参照している可能性が高いです。参照カウントがゼロにならない限り解放されないので、残っている参照を探すのが近道です。

Q. 循環参照はどう解放されますか?
A. お互いを参照し合うオブジェクトはカウントがゼロにならず、循環参照GCが後から回収します。即時ではないので、必要なら gc_collect_cycles() を呼ぶ手もあります。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です