PHP でのメモリ管理について

最近、メモリを大量に使うプログラムを組んでいまして、メモリ管理にかなり困っていました。 自分では全然メモリを使っているつもりはないのに、すぐにメモリ不足になってしまうのです。 自分が思っていないタイミングで予期せず大量に確保されたり、思っていたタイミングでメモリが解放されなかったりして、 全然思ったようにならなかったのです。 この沼はかなり深い気がするので、ハマったポイントを備忘録としてメモしておきます。 メモリをシビアに管理しようとするとなかなか大変です。

思っていないタイミングでメモリが確保されるケース

まずはこれ。 予期せずメモリが大量に確保されるケースです。

下記のコードを見てください。 これは、関数を呼ぶ際の引数を渡すところで、変数に改行コードを追加をして渡しているコードです。 よくある普通のコードだと思います。 $large には、めちゃくちゃ長い文字列が入っていると仮定してください。 例えば、めちゃくちゃ大きいファイルの中身が入っていて、数メガバイトあるような文字列だとしてください。 このコードを実行すると、関数 hoge() を呼んだ瞬間にメモリが大量に確保されます。

このコードのいったい何が問題なのかわかりますでしょうか?

PHP
<?php

$large = '(省略:めちゃくちゃ長い文字列)';

hoge($large . "\n");

原因

実はこれ、hoge() 関数を呼んだ時点で、 メモリ上では $large$large . "\n" の2つ分のメモリが必要になっています。 この $large の中身が小さければ、このコードでも問題はないのですが、 もし、この $large の中身がめちゃくちゃ長い文字列(例えば、めちゃくちゃ大きいファイルの中身)のようなものであった場合、 このような呼び方をしただけで、必要なメモリ量が $large の約2倍となり、メモリ不足に陥る原因になることがあります。 ただ改行コード "\n" を追加しているだけなので、1 byte 増えるだけだろうぐらいに思っていると痛い目に合います。 $large$large . "\n" は別物ですので、 関数の引数用として新たに $large . "\n" のためのメモリを確保しなければならないのです。

実際にこれが原因でメモリ不足に陥るプログラムを偶然書いてしまい、しばらく原因がわからなくてハマってしまいました。 全然メモリを使ってるつもりがないのに、関数の中に入った瞬間に使用しているメモリが大量に増え、 関数から抜けたら減るのですから、そりゃ普通は気付きませんよという話です。 とはいえ、考えてみれば当然と言えば当然のような気がしますね。

解決策

解決策としては、同じ1つの変数として処理できるなら処理しておくということです。 別物として扱うからその分の余分なメモリが必要になるのであって、 同じものとして扱えるのであれば必要最低限のメモリで済みます。

PHP にはコピーオンライト(Copy-On-Write)という仕組みがあり、 書き込まれたときだけコピーして新たにメモリを確保するという仕組みになっているため、 同じ変数を参照しているだけであれば、新たにメモリが確保されることはありません。 まぁ、変数の中身が小さければこんなことは別に気にしなくても良いと思うのですが、 めちゃくちゃに大きい場合は気にした方が良いかもしれません。

PHP
<?php

$large = '(省略:めちゃくちゃ長い文字列)';

// 引数に渡す前に処理しておく
// これなら、改行コード分増えるだけ
$large .= "\n";

hoge($large);

思っていたタイミングでメモリが解放されないケース

もう一つのケースは、思っていたタイミングでメモリが解放されないケースです。 適当にコードを書いていると、メモリがいつまでたっても解放されないという現象に出会うことになります。

下記のコードを見てください。 ① で確保した画像のメモリが、② の 関数 destroy() で解放されることを期待しています。 ですが、実際には、そのタイミングでは画像のメモリは解放されません。 では、実際にどのタイミングで画像のメモリが解放されるかわかりますでしょうか?

PHP
<?php

$image = imagecreatetruecolor(2000, 2000);	// ①

destroy($image);	// ②

function destroy(GdImage $image)
{
	// 画像のメモリを解放
	imagedestroy($image);
	$image = null;	// ③
}

原因

imagedestroy() は、実はPHP 8.0 以降は何も処理をしない(呼んでもメモリを解放しない)のですが、念のため書いてます。 実際には、その下の ③ の $image = null; で、メモリが解放されることを期待して書かれたコードです。

PHP の変数にはリファレンスカウントという、その変数が参照されている回数が記録されていて、 そのリファレンスカウントが 0 になったら、もうその変数は使用されていないということになり、 使用していたメモリが解放されるという仕組みになっています。 そこで、$imagenull を代入にすることで、リファレンスカウントを 0 にして画像のメモリを解放させようとしています。 unset() でも良いのですが、null を代入した方が処理速度が速いのでそうしています。

しかし、期待とは裏腹にこの ③ のタイミングでは画像のメモリは解放されません。 何故なら、呼び出し元 ① の $image がまだ残っており、リファレンスカウントが 0 になっていないのです。 関数の中でメモリを解放しようとしても、呼び出し元に戻るまでメモリが解放されないのです。

というか、このコード、実は最後の最後までずーーっと画像のメモリは解放されません。 なので、「どのタイミングで画像のメモリが解放されるか?」の答えは「解放されない」ということになります(もしくは PHP 終了時点)。

解決策1:呼び出し元でちゃんとクリアする

解決策の1つ目の方法は、呼び出し元でリファレンスカウントをちゃんとクリアしてあげることです。 例えば、戻り値で null を返して呼び出し元の変数を上書きするとか、明示的に null を代入するなどです。 ただこの方法では、結局のところ呼び出し元に戻るまでメモリが解放されることはないので、関数の中ですぐにメモリを解放したい場合は別の方法をとる必要があります。

例:戻り値で null を代入する場合
PHP
<?php

$image = imagecreatetruecolor(2000, 2000);

$image = destroy($image);	// 戻り値を受け取ったタイミングで解放される

function destroy(GdImage $image)
{
	// 画像のメモリを解放
	imagedestroy($image);
	$image = null;	// ここでは解放されない

	return $image;
}
例:明示的に null を代入する場合
PHP
<?php

$image = imagecreatetruecolor(2000, 2000);

destroy($image);
$image = null;	// このタイミングで解放される

function destroy(GdImage $image)
{
	// 画像のメモリを解放
	imagedestroy($image);
	$image = null;	// ここでは解放されない
}

解決策2:関数による階層構造にしない

解決策の2つ目の方法は、関数による階層構造を可能な限りしないことです。 リファレンスカウントが呼び出し元に残っているからメモリが解放されないのであれば、呼び出し元に残さなければ良いのです。 1つの関数の中で全てを処理してしまえば良いのです。

ただこれは、コードをメンテナンスするのが難しく、 わざとそういう書き方をしているということを忘れてしまったりすると、 すぐに思ったタイミングでメモリが解放されなくなることでしょう。 関数の中で、すぐに別の関数を呼びたくなるものです。 根本的な解決策とは言い難いかもしれません。

良い点としては、わざわざ明示的にメモリを解放しなくても、 関数を抜けた時点でリファレンスカウントが 0 になるので、 勝手にメモリが解放されることでしょうか。

PHP
<?php

draw();

function draw()
{
	$image = imagecreatetruecolor(2000, 2000);

	// 画像のメモリを解放
	imagedestroy($image);
	$image = null;	// このタイミングで解放される(処理しなくても良い)
}

解決策3:参照渡しにして、関数内から書き換える

解決策の3つ目の方法は、呼び出し元の変数を参照渡しにして、関数内から書き換えることです。 参照渡しであれば、関数の中で null にしたタイミングでメモリが解放されます。 参照渡しは少しトリッキーなコードになりがちなので、コードをメンテナンスするのが少し難しくなるのが欠点と言えば欠点かもしれません。

PHP
<?php

$image = imagecreatetruecolor(2000, 2000);

destroy($image);

function destroy(GdImage &$image)
{
	// 画像のメモリを解放
	imagedestroy($image);
	$image = null;	// このタイミングで解放される
}

参考サイト

関連記事

プログラムを書いていると気になるのがコードの読みやすさです。複数人で開発している場合などは特にコードの読みやすさは重要です。PHP_CodeSniffer とは?PHP_CodeSniffer は、PHP のコードがちゃんとコーディング規約に沿って記述されているか検査したり、コーディング規約に違反している個所を自動的に ...
PHP のコードを実行する前に、バグがあるかどうか調べられると便利だとは思いませんか?PHP はスクリプト言語ですので、いくら文法的に正しいコードであっても、実際に実行させるまでバグか発生するかどうかわからないという、スクリプト言語であるが故の本質的な問題を抱えています。C や Java など他のコンパイル言語ではコン ...
PHP_CodeSniffer や PHPStan などで、コードの文法的な正しさは確認できますが、そのコードが本当に正しい動作を行っているかどうかを確認するためには、やはり最終的には動作させてみるしかありません。PHPUnit とは?PHPUnit は、PHP のテストフレームワークです。その名前からわかる通り、基本 ...

記事検索

最新記事

人気記事

RSSフィード

お知らせ

フィードバック

要望などあれば、お気軽にどーぞ。 不具合やバグを発見した場合も、連絡をいただけると助かります。

匿名でフィードバックする
匿名でフィードバックする

要望などあれば、お気軽にどーぞ。 不具合やバグを発見した場合も、連絡をいただけると助かります。

なお、このフォームから入力された内容について、管理者から返信はできませんので注意してください。 もし、管理者からの返信が必要であれば、X(Twitter) もしくは、お問い合わせより、お願いします。

  • フィードバックの送信が完了しました。