PHPのガベージコレクション / メモリリーク・メモリオーバーを起こさないために

## はじめに
これまで1年と少し、PHPは基本的にはメモリを気にしなくても問題無いという何となくの認識でさわってきたが、改めてメモリの確保と解放の仕組みについて、またどのような場合にメモリリーク、そしてメモリオーバーを起こしてしまう可能性があるのか勉強したので、まとめておく。

## ガベージコレクション（GC）とは

ガベージコレクション（GC）とは、プログラムが動的に確保したメモリ領域（変数等で用意したメモリ領域）のうち、**不要となった領域を自動的に解放**し、空き領域として再利用できるようにする機能のこと。

現在は多くのプログラミング言語で採用されている。

（C、C++、Rust等、GCを採用していない言語も存在する。）

GCの機能が開発される以前のプログラムでは、プログラマが明示的にメモリの解放を行う必要があり、解放コードを書き忘れるとメモリ上から解放されず、空き領域が逼迫する要因となっていた。

GCが開発されたことで、**プログラマがメモリの確保と解放を考慮する必要**や**メモリリークのリスク**が*ほとんど*無くなった。

## PHPが変数を管理する仕組み（zval）

PHPでは「zval container」という、**変数の実データ**と**参照情報**の属性を持つ構造体で変数を管理している。

> **PHP 変数は「zval」と呼ばれるコンテナに保管されます。 zval コンテナには、変数の型と値の他に、情報の追加ビットを2つ含みます。** 1つ目は「is_ref」と呼ばれ、変数が「参照集合」の一部かどうかを示すブール値 です。 このビットによって、通常の変数と参照を区別する方法を PHP エンジンが知ります。 &演算子によって作成されるように、PHP ではユーザーランドで参照を使えるので、 zval コンテナもメモリー使用状況を最適化するための内部的なリファレンスカウント機構を 持ちます。 **追加情報の2つ目は「refcount」と呼ばれ、この1つの zval コンテナをどれだけ多くの 変数名（シンボルとも呼ばれます）が指すかを含みます。**
> 
[PHP: リファレンスカウントの原理 - Manual](https://www.php.net/manual/ja/features.gc.refcounting-basics.php)

zvalの属性の中で、GCにおいて重要なのが「**refcount**」という属性。

## PHPのGCが「不要になったメモリ領域」を判別する方法

GCは不要となった領域を**自動的に**解放してくれるが、何をもって**不要になった**と判別するのか？

PHPのGCでは、**リファレンスカウント方式**が用いられている。

> **「refcount」がゼロに達すると、変数コンテナは破棄**されます。 変数コンテナにリンクされたあらゆるシンボルが**スコープを抜ける (たとえば関数が終わる) 場合、またはシンボルへの代入が解除された (たとえば [unset()](https://www.php.net/manual/ja/function.unset.php) が呼ばれた) 場合に「refcount」が減少します。**
> 

[PHP: リファレンスカウントの原理 - Manual](https://www.php.net/manual/ja/features.gc.refcounting-basics.php#:~:text=%E5%8D%81%E5%88%86%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88%E3%81%A7%E3%81%99%E3%80%82-,%E3%80%8Crefcount%E3%80%8D,-%E3%81%8C%E3%82%BC%E3%83%AD)

つまり、スコープを抜けるときやunset()で代入が解除されたときにrefcountの数が減った結果、refcountの値が0になったら、その変数コンテナ（zval）が破棄される。

## メモリの確保と解除の挙動を見てみる

### 使用するメソッド

- **xdebug_debug_zval()**
    
    zvalに格納されている実際の値と、refcount・is_refの値を出力する。
    
- **memory_get_usage()**
    
    現在の PHP スクリプトに割り当てられたメモリの量をバイト単位で返す。
    

### 試してみる

```php
//web.php

Route::get('/index', [TestController::class, 'index']);
```

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Log;
use stdClass;

class TestController extends Controller
{		

	public function index()
    {
        $this->test();

        echo "test()終了後：" . memory_get_usage();  //test()終了後：4049200
						// test()の関数を抜けると、$aも解放されたと読み取れる。
    }

    private function test()
    {
        echo "１：" . memory_get_usage();  //１：4046528

        $a = [];
        for ($i = 0; $i < 10000; $i++) {
            $a[$i] = new stdClass();
        }

		xdebug_debug_zval('a');  //a:(refcount=1, is_ref=0)array (size=10000)
        echo "２：" . memory_get_usage();  //２：5103936

        $b = [];
        for ($i = 0; $i < 10000; $i++) {
            $b[$i] = new stdClass();
        }

		xdebug_debug_zval('a');  //a:(refcount=1, is_ref=0)array (size=10000)
        xdebug_debug_zval('b');  //b:(refcount=1, is_ref=0)array (size=10000)
        echo "３：" . memory_get_usage();  //３：6169048

        unset($b);  //$bを解放。$aは残っている。

		xdebug_debug_zval('a');  //a:(refcount=1, is_ref=0)array (size=10000)
        xdebug_debug_zval('b');  //b: no such symbol
        echo "４：" . memory_get_usage();  //４：5235008

        $c = [];
        for ($i = 0; $i < 10000; $i++) {
            $c[$i] =  new stdClass();
        }

		xdebug_debug_zval('a');  //a:(refcount=1, is_ref=0)array (size=10000)
        xdebug_debug_zval('c');  //c:(refcount=1, is_ref=0)array (size=10000)
        echo "５：" . memory_get_usage();  //５：6169048

        unset($c);

		xdebug_debug_zval('a');  //a:(refcount=1, is_ref=0)array (size=10000)
        xdebug_debug_zval('c');  //c: no such symbol
        echo "６：" . memory_get_usage();  //６：5235008
    }
```

## メモリリークが起こるとき

「スコープを抜けるときやunset()で代入が解除されたときにrefcountの数が減った結果、**refcountの値が0**になったら、その変数コンテナ（zval）が破棄される」ということが分かった。

だが、下記のように変数が互いに参照している状態（循環参照）になると、スコープを抜けたりunset()で代入を解除しても、**refcountが0にならず**変数コンテナが破棄されない状況が起きてしまう。

```php
<?php

class TestClass
{
    private $data;

    public function setData($data)
    {
        $this->data = $data;
    }
}

$class1 = new TestClass;
$class2 = new TestClass;

$class1->setData($class2);
$class2->setData($class1);  //互いに参照する状態
```

PHP5.2まででは、上記のように参照関係に循環があるとメモリの解放を自動で行うことができなかったが、PHP5.3から循環参照の問題にもGCが対応するようになった。

> 伝統的に、PHP で以前使われていたようなリファレンスカウント記憶機構では、 循環参照メモリ・リークに対処できません。 しかしながら、5.3.0 現在、PHP ではその問題に焦点を当てた [» Concurrent Cycle Collection in Reference Counted Systems](https://pages.cs.wisc.edu/~cymen/misc/interests/Bacon01Concurrent.pdf)
 レポートに由来する同期アルゴリズムを実装しています。
> 

[PHP: 循環の収集 - Manual](https://www.php.net/manual/ja/features.gc.collecting-cycles.php)

### PHP5.3以降の循環参照に対するGC

- refcountが減ったがまだ0ではない場合、本当は不要なごみができてしまった「かもしれない」候補としてルート・バッファに記録しておく。
- ルート・バッファが満杯になると（一万件に達すると）、ガベージ・サイクルのチェックを実行して、「refcountが減ったがまだ0ではない」ものが本当は不要なものなのか調べる。

> ガベージサイクルで、それらの refcount を減少させられるかどうかチェック することによって、どの部分がゴミか発見できます。
> 

> refcount の減少が起こりうるたびにガベージ・サイクルのチェックを呼び出さなくても良いように、 代わりに、可能性があるルート (zvals) 全てをアルゴリズムは**ルート・バッファにたくわえます**。 （それらは「紫」とマークされます) それは、可能性のあるガベージのルートそれぞれがバッファ内で一度だけで終わることも確認します。 **ルート・バッファが満杯の場合だけ、内部のそれぞれの zval 全てに対して収集機構が動き出します**。
> 

> ガベージコレクタがオンの場合、 ルート・バッファが満杯になるといつでも、先に述べたように循環検出法が実行されます。 **ルート・バッファでは、可能性があるルートのサイズが一万件に固定されています**。 （PHP のソースコードの `Zend/zend_gc.c` で `GC_ROOT_BUFFER_MAX_ENTRIES` 定数を変更して、PHP を再コンパイルすると変更できます） ガベージコレクタをオフにすると、循環検出法は実行されなくなります。
> 

> デフォルトで、PHP のガベージコレクタは有効です。 しかしながら、これを変更できる php.ini の設定があります。 それが [zend.enable_gc](https://www.php.net/manual/ja/info.configuration.php#ini.zend.enable-gc) です。
> 

PHP5.3以降では、**自動でガベージサイクルが実行されるのを待たず強制的に実行する**ことができる、 **`gc_collect_cycles`** というメソッドも用意されている。

> (PHP 5 >= 5.3.0, PHP 7, PHP 8)
> gc_collect_cycles — すべての既存ガベージサイクルを強制的に収集する
> 

[gc_collect_cycles](https://www.php.net/manual/ja/function.gc-collect-cycles.php)

## メモリリーク、メモリオーバーを起こさない為に

- スコープ内で、多大なメモリを使用する処理を行わない。
    
    必要ならばunset()でメモリを解放する。
    
- 循環参照を起こさない。
    
    PHP5.3以降はGCが循環参照にも対応しているが、ルート・バッファ（不要な領域の候補）が1万件溜まった時にのみ実行されるので。
    

### 参考記事
https://www.php.net/manual/ja/features.gc.refcounting-basics.php

https://www.php.net/manual/ja/features.gc.collecting-cycles.php

https://www.slideshare.net/y-uti/php-gc