♻️

【PHPメモリ管理】関数内の変数はいつ解放されるの?

2023/03/26に公開

はじめに

最近、私は会社の先輩からPHPのメモリ管理について教えていただきました。
特に、関数内の変数が解放されるタイミングについての知識は、パフォーマンスの向上に直結する重要な要素です。ですので今回は、解放されるタイミングについて、基本的な概念を皆さんに紹介したいと思います。

参考

https://www.php.net/manual/ja/features.gc.refcounting-basics.php

PHP変数の保管場所「zval」

まず、PHP変数はどこに保管されるのでしょうか?
PHP変数は zvalコンテナに保管されます。

zvalコンテナの構成

  • 変数の型
  • 変数の値
  • 情報の追加ビット
    • is_ref
    • refcount

is_ref:変数が「参照集合」の一部かどうかを示すbool値

  • このビットによって、通常の変数と参照を区分する方法をPHPエンジンが知ります。
  • PHPではユーザーランドで参照を使えるので、zvalもメモリー使用状況を最適化するための内部的なリファレンスカウント構造を持っています。

refcount:1つのzvalをどれだけ多くの変数名(シンボル)が指すか

  • 変数名(シンボル)はすべてシンボルテーブルに保管されます。
  • スコープごとに1つのシンボルテーブルがあります。
  • 関数やメソッドごとのスコープばかりではなく、メインスクリプト用スコープ(ブラウザによってリクエストされたスクリプト)もあります。

例1)zval 情報を確認する

<?php
$a = "new string";
xdebug_debug_zval('a');

新しい変数名(a)が現在のスコープで作成され、新しい変数コンテナがstring型と値(new string)で作成されます。

a: (refcount=1, is_ref=0)='new string'
  • is_ref : ユーザーランド参照が作成されたことがないので、is_refビットはデフォルトで false にセットされます。
  • refcount : この変数コンテナを利用する変数が1つだけあるので、refcountは1に設定されます。

refcountを持つ参照(is_refビットが true の場合)が1の場合、参照されていないかのように(= is_refが常に false であったかのように)扱われる点に注意してください。

例2)zval の refcount を増加

<?php
$a = new stdClass();
$b = $a;
xdebug_debug_zval( 'a' );
a: (refcount=2, is_ref=0)=class stdClass {  }

同じ変数コンテナがabにリンクされるので、refcount は2になります。

xdebug_debug_zval()で関数内の変数が解放されるタイミングを確認する

上記で記述したzvalを利用して関数内の変数のメモリはいつ解放されるか確認してみましょう。
テストのために値を受け取りつつ、何もせずに終了する関数を使用します。

<?php

/**
 * @param stdClass $p パラメータ
 * @return void
 */
function nothing(stdClass $p): void
{
  xdebug_debug_zval('p');
  return;
}

$a = new stdClass();
xdebug_debug_zval('a');

nothing($a);
xdebug_debug_zval('a');
a: (refcount=1, is_ref=0)=class stdClass {  }
p: (refcount=3, is_ref=0)=class stdClass {  }
a: (refcount=1, is_ref=0)=class stdClass {  }

実行結果を見てみましょう。

nothing($a);

で関数内の$p$aにリンクされている変数コンテナにリンクされるので、refcount が増加します。

p: (refcount=3, is_ref=0)=class stdClass {  }

その後、関数が終わる(スコープを抜ける)時点で、PHPは自動的に関数内の変数を解放しますので refcount が減少します。

a: (refcount=1, is_ref=0)=class stdClass {  }

この結果から、関数内(ローカルスコープ)の変数は関数終了時に解放されることが確認できました。ですので、私たちは関数内の変数のメモリについてはあまり意識しなくても良いかと思います。

Discussion