📄

2020/9/8 PHP・GCの話-4話)MemoryLeakと解除できない変数データ

2024/05/24に公開

この記事は2020/9/8に書きました。

前書き

  • すべての記事は、自分の勉強目的と主観の整理を含めています。あくまで参考レベルで活用してください。もし誤った情報などがあればご意見をいただけるととっても嬉しいです。
  • 内容では、省略するか曖昧な説明で、わかりづらいところもあると思います。そこは、連絡いただければ補足などを追加するので、ぜひ負担なくご連絡ください。
  • 本文での「GC」は、「Garbage Collection, Garbage Collector」の意味しており、略語として使われています。
  • この記事は、連載を前提に構成されています。

※ 連載目録

※ 連載で使うサンプルコード

Sample Code Link on Github

● ExampleGc.php : 2話から6話までの内容で使うサンプルコードです。
● ExampleWeakReference : 7話のWeakReferenceの内容で使うサンプルコードです。

本連載記事は、基本的にこのサンプルコードをベースに説明をしています。
サンプルコードは、必ず見る必要も実行してみる必要もありません。
各話ごとに、コードを分解して動作原理と結果を解説しますので、基本記事の内容で足りるように心がけます。
あくまで、全体コードをみたい、手元で回してみたい、修正して回してみたいという方向けです。

今回の話

今回は、以下のものを話そうと思うます。

    1. Memory Leakとは?
    1. 解除できない変数データの例 (循環参照)
    1. Summary

今回からは、本格的にサンプルコードを引用しながら、見ていきますので、
以下のリンクのコードを一緒に参考にしながら見ると良いと思います。

1. Memory Leakとは?

シンプルに言うと、

もう使えない変数データが、メモリを専有し続ける現象

ですね。これを「ゴミ・Garbage」と表現したりもします。

もうちょっと詳しく説いて行くために、Memory Leakの定義をwikiから引用すると、

https://en.wikipedia.org/wiki/Memory_leak#cite_note-1

① In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released.
② A memory leak may also happen when an object is stored in memory but cannot be accessed by the running code.

少し意訳すると

① コンピューター工学において、Memory Leakとは、プログラムで、もう使わないのに解除されないような、間違ったメモリ空間の割当により起きる、有限な資源の無駄遣いの現象です。
② 一例として、memory leakは、メモリに保存はされているものの、実行コード上ではこれ以上サクセスされないオブジェクトにより起きることもありえます。

ですね。

それ以外で一番多い事例は、ネットワークコネクションやグラフィックなどのresourceタイプの解除を忘れることだと思います。※1

②に対するMemory Leakの一つの例が、今から見ていく「循環参照」です。
引き続き、②の事例の「循環参照」に関して詳しく見ていきます。

2. 解除できない変数データの例

ここでの「解除できない」というのは、

プログラマーの意図的には、すでに解除しており、これ以上使われるはずのない変数の元データがメモリ上にの残り続けること

を意味します。

簡単な例えとしては、オブジェクト同士の「循環参照」があります。
簡単なコードで紹介すると以下のコードで再現できます。

$a = new \stdClass;
$b = new \stdClass;

//循環参照
$a->node = $b;
$b->node = $a;

//これ以上使われないはずなので、このケースは、メモリからデータは消滅しない
unset($a);
unset($b);

これがなぜ、問題になるのかを、サンプルコードと実行結果で解説します。

1) サンプルコード例

● クラス定義引用
Sample Code Link on Github

abstract class Base
{
    private $dummyData;
    private $tag = null;
    private $nodes = array();

    public function __construct($tag)
    {
        $this->tag = $tag;
        $this->dummyData = str_repeat('a', 20*1024*1024); //20M Byte size approximately
    }

    /**
     * add Reference as ChildNode
     */
    public function addNode(object $obj)
    {
        $this->nodes[] = $obj;
        return $this;
    }
}

class AliveInScope extends Base {}

class CircularReference extends Base {}

● コード引用
Sample Code Link on Github

    private function doExampleGcBasic()
    {
//...中略
        Log::debug(null, ['event' => 'new', 'msg' => 'V']);
        $alive = new AliveInScope('V');
//...中略
        Log::debug(null, ['event' => 'new', 'msg' => 'A, B']);
        $circleA = new CircularReference('A');
        $circleB = new CircularReference('B');

        Log::debug(null, ['event' => 'set', 'msg' => '$alive`s reference to A']);
        $circleA->addNode($alive);

        Log::debug(null, ['event' => 'set', 'msg' => 'circluar reference on A B']);
        $circleA->addNode($circleB);
        $circleB->addNode($circleA);

        xdebug_debug_zval('alive');
        xdebug_debug_zval('circleA');
        xdebug_debug_zval('circleB');
        $this->logMemUsage();

        Log::debug(null, ['event' => 'unset', 'msg' => 'A, B']);
        unset($circleA);
        unset($circleB);

        xdebug_debug_zval('alive');
        xdebug_debug_zval('circleA');
        xdebug_debug_zval('circleB');
        $this->logMemUsage();
//...中略

● 実行結果引用

root@bc290870f5e9:/var/www/html/subdomain/laravel# ./artisan example:gc | cut -d "$" -f 1

[2020-09-07 20:30:37] local.DEBUG:  {"event":"new","msg":"V"}
alive: (refcount=1, is_ref=0)=class App\Console\Commands\AliveInScope { private 
[2020-09-07 20:30:37] local.DEBUG:  {"Memory Usage(Bytes)":"37,060,552"} 
[2020-09-07 20:30:37] local.DEBUG:  {"event":"new","msg":"A, B"}
[2020-09-07 20:30:37] local.DEBUG:  {"event":"set","msg":"
[2020-09-07 20:30:37] local.DEBUG:  {"event":"set","msg":"circluar reference on A B"}
alive: (refcount=2, is_ref=0)=class App\Console\Commands\AliveInScope { private 
circleA: (refcount=2, is_ref=0)=class App\Console\Commands\CircularReference { private 
circleB: (refcount=2, is_ref=0)=class App\Console\Commands\CircularReference { private 
[2020-09-07 20:30:37] local.DEBUG:  {"Memory Usage(Bytes)":"79,015,032"} 
[2020-09-07 20:30:37] local.DEBUG:  {"event":"unset","msg":"A, B"}
alive: (refcount=2, is_ref=0)=class App\Console\Commands\AliveInScope { private 
circleA: no such symbol
circleB: no such symbol
[2020-09-07 20:30:37] local.DEBUG:  {"Memory Usage(Bytes)":"79,015,672"} 

2) コードと実行結果の解説

        Log::debug(null, ['event' => 'set', 'msg' => 'circluar reference on A B']);
        $circleA->addNode($circleB);
        $circleB->addNode($circleA);
[2020-09-07 20:30:37] local.DEBUG:  {"event":"set","msg":"circluar reference on A B"}
...中略
[2020-09-07 20:30:37] local.DEBUG:  {"Memory Usage(Bytes)":"79,015,032"} 

ソスコード上の上記のポイントで、AとBは、お互いの内部でお互いを参照することになります。
その後のメモリの使用量は「79,015,032」Bytesになっています。

        Log::debug(null, ['event' => 'unset', 'msg' => 'A, B']);
        unset($circleA);
        unset($circleB);
[2020-09-07 20:30:37] local.DEBUG:  {"event":"unset","msg":"A, B"}
alive: (refcount=2, is_ref=0)=class App\Console\Commands\AliveInScope { private 
circleA: no such symbol
circleB: no such symbol
[2020-09-07 20:30:37] local.DEBUG:  {"Memory Usage(Bytes)":"79,015,672"} 

unsetをして、A,Bは、これ以上使うこともできないのに、なぜかメモリの使用量は減っていません。
つまり、使うこのもないのに、有限であるメモリ空間をずっと専有していることになります。

なぜ、メモリの使用量は減らずに、実のデータがずっと残り続けるのでしょう。

3) コードの実行時に起きる変数と参照カウントの変化解説(GIF)

①循環参照変数の生成の段階の変化

上記のイメージの4番目のように、AとBは、$circleA, $circleBの参照以外に、各自の内部で、お互いを参照するようになります。
だとしたら、$circleA$circleBをunsetし、変数を無効にしたらどうなるのでしょう。

$circleA$circleBをunsetした後の段階の変化 (絵が間違っている)

前回の「変数が消滅しない条件」は

「実際のデータの参照が一つ無効になる」時、参照カウント(refcount)が「1以上」であれば、データは消滅せずに残り続ける。

でした。

絵の2番めのように、$circleA$circleBの変数を解除したことで、2つとも実のデータにアクセスできなくなりました。
しかし、元のデータはお互いを参照しているので、参照カウントは2→1になりますが、その時の参照カウントが1以上であるため、「どこかで使われている」とシステムでは認識し、メモリから解除することはできなくなります。

なので、プログラマー的には、消滅してほしいデータであるにも関わらず、ずっとメモリに残り続けることになります。

まさに、Memory Leakとして話した、もう使えない変数データが、メモリを専有し続ける現象であり、ゴミ・Garbageですね。

だとしたら、このゴミ問題を解決するために、PHPではどういう機能を提供しているのでしょうか。

その一つが、次回に登場するGC・Garbage Collectionになります。
そこは、次回に詳しく説明することになります。

3. Summary

今回で、最低限に覚えて頂くと良い内容は以下になります。

  • Memory Leakとは、「もう使えない変数データが、メモリを専有し続ける現象」であり、「ゴミ」が残る現象
  • 循環参照は、Memory Leakのわかりやすい例であり、GCが収集する対象としてのわかりやすい一例
  • このゴミ問題を解決するために、PHPではどういう機能の一つが、GC・Garbage Collection

後書き

1話から今回まではGCの背景になる、変数の仕様・動作・消滅メカニズムなどを話してきました。

次からは、本格的にGCのメカニズムに対して話していきます。

GCの発生条件や明示的に発生させる方法、GCが起きたら行われるメカニズムを解説していきます。この時に、zvalと参照カウントの理解と、変数の消滅基準、消滅せず残り続けるデータの理解が必要なので、それを意識した上でご覧頂けると嬉しいです。

説明とは不足なところか、分かりづらいところはあるかもですが、フードバック頂けると補足とか訂正いたしますので、宜しくお願いします。

※注釈

※1
▶ resourceタイプの解除を忘れること

実のところ最近は、resourceタイプに対しても使われなくなったら自動解除してくれたりします。しかし例外な場合も無いわけでは無いのでresourceタイプ(または違う言語での類似タイプ)の解除は意識しておくと良いです。!

Discussion