PHP・GCの話-5話)GC登場。GC発生条件とROOT BUFFER



PHP・GCの話-5話)GC登場。GC発生条件とROOT BUFFER

前書き

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

※ 連載目録

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

Sample Code Link on Github

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

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

今回の話

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

    1. GCの登場。Memory Leakを解決するための機能
    1. PHPでGCが発生する条件
    1. root buffer
    1. Summary

1. GCの登場。Memory Leakを解決するための機能 ※1

GC(Garbage Collection)とは、一言で「メモリ内のゴミ自動回収仕組み」と言えます。

前回での話では、メモリ内のゴミ問題を解決するための機能の一つがGarbage Collectionだと話ていました。

これからのメインテーマになります。言葉通りに、ゴミを回収してくれるという意味の機能ですね。

GCは、下のGIFの例えのように、とあるタイミングで、もう使えないのに残っているメモリ内の専有データをまとめて収集し解除してくれます。

2. PHPでGCが発生する条件

PHPでGCが発生する条件は以下の2つがあります。

1) root buffer内に、root zvalの数が上限に達した場合

phpにはroot bufferという空間があります。
そこが最大値まで達すると、GCが発生し、ゴミデータの収集を実行することになります。

一時的にガラクタをおいておく倉庫みたいなものですね。ガラクタ倉庫にいらないと思われるものを入れておき、空間がなくなったら、いらないものを整理し、必要なものはまたおいておくのと、全く同じ概念です。

2) 明示的にGCのcycleを呼び出す

phpでは、GCを明示的に起動できる、以下の関数を提供しています。
しかし、本当に必要と思われる時以外は、使うことはおすすめしません。※2

1
gc_collect_cycles ( void ) : int

3. root buffer

では、root bufferに関して詳しく見てみましょう。

1) root bufferとは

①今は、解除できない変数データのroot zvalが保存される空間

root zvalとは、変数シンボルと直接繋がれているzvalを言います。
上記のオブジェクトの例で、$objと直接つながっているzvalがそうです。
(上記の絵は実際の構造を簡略化したものなので参考までにご覧ください)※3

root bufferは、該当する変数シンボルが解除される時、参照カウントが1以上で、すぐメモリから解除できない変数データと紐付くroot zvalroot bufferに入れてマーキングして保存しておく仕組みになっています。

②GCがチェックする対象を絞り込むための仕組み

root zvalroot bufferに入れる一番の目的は、「GCがチェックする対象を識別」することです。
root bufferの仕組みにより、GCは「何をチェックすればいいのか」を判別ができるようになり、すべてのzvalをチェックする必要はなくなります。

2) root bufferとGC_ROOT_BUFFER_MAX_ENTRIES

root bufferはbufferである以上、その許容量に上限があります。基本10000個で設定されています。
この設定はphp complieオプションで指定可能です。

1
2
3
4
// https://github.com/source-comment/php7/blob/1c211996565830feab036cc1daf36a3bed5647e8/Zend/zend_gc.c#L76
// file : Zend/zend_gc.c

#define GC_ROOT_BUFFER_MAX_ENTRIES 10001

3) root bufferが完全に貯まるとGCが発動する

root bufferが、上記でみた上限に達すると、GCが発動し、使われないメモリ内のデータを回収します。
本記事では、サンプルコード内で、以下のようなメソッドで再現しています。

Sample Code Link on Github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* invoke Garbage Collection to make & unset 10000 of self referenced object's zval.
* it store to Root Buffer because refCount is 1 Altough life time is ended because refered by itself.
* When Root Buffer is filled to 10000, Garbage Collection will occur if GC_ROOT_BUFFER_MAX_ENTRIES=10000
*/
private function makeGarbageReferenceTo10000CountWhichPHPDefaultRootBufferMax()
{
$PHP_DEFAULT_GC_ROOT_BUFFER_MAX_ENTRIES = 10000;

for($i = 0; $i < $PHP_DEFAULT_GC_ROOT_BUFFER_MAX_ENTRIES; $i++) {
$a = new \stdClass;
$a->selfRef = $a;
}
}

実行結果例

root bufferを10000以上までためたことでGCが発動し、メモリの使用量が約79Mから37Mまでに減っています。

Sample Code Link on Github

1
2
3
4
5
6
7
8
9
10
11
public function handle()
{
...
ini_set('memory_limit', '256M');
Log::debug(null, ['memory_limit' => ini_get('memory_limit')]);
...
$this->doExampleGcBasic();
...(doExampleGcBasicメソッド内)
Log::debug(null, ['event' => 'invoke', 'msg' => 'garbage collection']);
$this->makeGarbageReferenceTo10000CountWhichPHPDefaultRootBufferMax();
$this->logMemUsage();
1
2
3
4
5
6
7
[2020-09-12 19:05:03] local.DEBUG:  {"memory_limit":"256M"} 
...
[2020-09-12 19:05:04] local.DEBUG: {"Memory Usage(Bytes)":"79,016,328"}
[2020-09-12 19:05:04] local.DEBUG: {"event":"invoke","msg":"garbage collection"}
alive: (refcount=1, is_ref=0)=class App\Console\Commands\AliveInScope { private
[2020-09-12 19:05:04] local.DEBUG: {"Memory Usage(Bytes)":"37,612,000"}
[2020-09-12 19:05:04] local.DEBUG: {"event":"end","msg":"App\\Console\\Commands\\ExampleGc::doExampleGcBasic"}

4) root bufferと、メモリ、データサイズは無関係

root bufferは、あくまでroot zvalの数だけをチェックします。
データのサイズはチェックしないので、root bufferが上限になる前にmemory limitを超えると、プログラムはダウンしてしまうので注意が必要です。もっと詳しく確認したい方は、サンプルコードのmemory_limitの設定を変えてお試しください。

実行結果例:memory_limit = 64M

メモリが足りず、GC発動までも行かずメモリ関連Fatal errorになりました。

1
2
3
4
5
public function handle()
{
...
ini_set('memory_limit', '64M');
...
1
2
3
4
[2020-09-12 19:09:17] local.DEBUG:  {"memory_limit":"64M"} 
...
[2020-09-12 19:09:17] local.DEBUG: {"event":"new","msg":"V"}
PHP Fatal error: Allowed memory size of 67108864 bytes exhausted (tried to allocate 83886112 bytes) in /var/www/html/subdomain/laravel/app/Console/Commands/ExampleGc.php on line 130

4. Summary

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

  • GCは、Memory Leakを解決するための機能の一つ
  • PHPには、GCのための、root bufferという倉庫空間があり、今は解除できない便数のroot zvalを格納する
  • root bufferに10000のroot zvalが貯まるとGCは発動する。

後書き

今回は、GCの定義を少し振り返ることと、GCの発動条件と、PHPのGCの核心概念であるroot bufferに関して見ました。
実はこの5話までの内容が色々プログラミング知識のためになる内容だと思います。

次の話では、GCが実際にどういう処理でメモリ回収を行うのかに関してお話しますが、PHPの内部的な話であり、プログラマーが制御できる機能ではないので、実戦ではあんまりやくに立たない話かもしれません。

しかし、GCに関してもっと奥深く理解できる機会、コア機能開発に対するアーキテクチャー設計にも役に立つ話になるとも思います。

そして次回の話でGCの解説は終わり、もう一話でGC StatisticsとWeak Reference Typeを簡単に話をした後、完結になります。

※注釈

※1
▶ GCの登場。Memory Leakを解決するための機能

正しくは「緩和する」が正確な表現かもしれません。実はGCは、Memory Leakを完全に解決する解決策ではなく、あくまで減らしてくれる仕組みからです。

※2
▶ 本当に必要と思われる時以外は、使うことはおすすめしません。
その理由としては、0話の「GCは万能の神様ではない」という内容で話していますので、もしよろしければご参考ください。

※3
▶ (上記の絵は実際の構造を簡略化したものなので参考までにご覧ください)
scalar, array, objectのタイプとPHPバージョンの違いとかにより、シンボルとzval構造体、zval value構造体、zval object構造体などなどの構成図は色々変わってきます。少し昔の資料になりますが、もっと詳しく見たい方は以下のリンクを参考にするといいと思います。
https://yokkuns.hatenadiary.org/entry/20090614/1244994082