ガベージ・コレクタの挙動を実験してみた

.Net Framework 4.5.2のガベージ・コレクタの挙動を調べるために、C#のコードで実験してみた記事です。

※ 本記事のサンプルコードは、川俣晶氏の以下記事を参考にさせていただきました。

* [.NET TIPSガベージ・コレクタを明示的に動作させるには？](http://www.atmarkit.co.jp/fdotnet/dotnettips/021gc/gc.html)

#ローカル変数の場合

以下コードでは、`Queue<string[]>`型のオブジェクトをローカル変数として宣言します。キューに巨大データを詰め込んで拡張した後、`GC.Collect()`でマネージドヒープのゴミ回収をしますが、`GC.Collect()`前後でのメモリ使用量（近似値）を出力しています。

```csharp:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GCTest
{
    class Program
    {
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            var q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
        static Queue<string[]> CreateQueue()
        {
            var q = new Queue<string[]>();

            string[] a = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                a[i] = new String('A', 1000);
            }
            q.Enqueue(a);

            string[] b = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                b[i] = new String('A', 1000);
            }
            q.Enqueue(b);

            string[] c = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                c[i] = new String('A', 1000);
            }
            q.Enqueue(c);

            string[] d = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                d[i] = new String('A', 1000);
            }
            q.Enqueue(d);

            return q;
        }
    }
}
```

リリースモードで実行すると、結果は以下となりました。

```csharp:実行結果（リリースモード）
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :40124
```

`GC.Collect()`によって7.7MBのメモリが解放されたのは分かるのですが、`GC.Collect()`の呼出し時点では`q`はスコープを抜けてないのだから、メモリ解放されるのおかしくね？と思いました。

ですが、これって`GC.Collect()`から`q`のスコープ末尾「`}`」までの間に、`q`が使用されていないから解放されたのかなあと思います。

試しに、`q`を使用するコードを追加してみました。

```csharp:qを使用してガベコレの対象から除外
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            var q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            q.Count(); // qを参照
            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
```

`q`の使用を追加した上記コードをリリースモードで実行してみると、ライフタイムが延長されたのか、メモリ解放されませんでした。

```csharp:実行結果（リリースモード）
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :8120136
```

ちなみにデバッグモードで実行すると、最適化されないせいか、`q`を使用しなくてもライフタイムが延長せず、メモリも解放されていませんね。

```csharp:実行結果（デバッグモード）
The highest generation is 2.
1. Before CreateQueue() :31772
2. After CreateQueue()  :8113124
3. After GC.Collect()   :8111892
```

#GC.KeepAlive()

`GC.KeepAlive()`は、オブジェクトのライフタイムを延長します。このメソッドは「この時点までライフタイムを延長します」とマーカするものです。

```csharp:GC.Keepalive()でライフタイムを延長
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            var q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            GC.KeepAlive(q); // この時点までqのライフタイムを延長

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
```

ライフタイムが延長され、予想通りガベコレの回収対象になりませんでした。

```:実行結果（GC.KeepAlive()を使用）
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue()  :8219792
3. After GC.Collect()   :8224688
```

# オブジェクトのライフタイムを制限

ブロックで閉じてオブジェクトのライフタイムをexpiredさせれば、デバッグモードでもガベコレに回収されんじゃね？と思い、以下のコードを書いてみました。オブジェクト`q`を`{}`内に閉じ込めています。

```csharp:スコープで閉じる（デバッグモード）
        static void Main(string[] args)
        {
            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
            {
                var q = CreateQueue();
            }
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");

            GC.Collect();

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
```

あれ、解放されませんね…。デバッグモードってそういうものなんですね。

```:実行結果（デバッグモード）
The maximum generation is 2.
1. Before CreateQueue() :32088
2. After CreateQueue()  :8123664
3. After GC.Collect()   :8112208
```

ただし、`GC.Collect()`の前に`q`に`null`を代入すると、デバッグモードでも`q`が使用するリソースは解放されました。

```csharp:nullを代入
        static void Main(string[] args)
        {
            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
            {
                var q = CreateQueue();
                Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
                q = null;
            }
            GC.Collect();

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
```

```:実行結果（デバッグモード）
The maximum generation is 2.
1. Before CreateQueue() :32088
2. After CreateQueue()  :8123664
3. After GC.Collect()   :32100
```

#static変数の場合

`q`をstatic変数にしてみました。

```csharp:static変数にしてガベコレ対象から除外
        static Queue<string[]> q;
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
```

するとどうでしょう。`q`はガベコレに回収されませんでした。まあ、`static`なのだからそりゃそうですね。

```csharp:実行結果（リリースモード）
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :8120136
```

#static変数の場合（nullを代入）

`q`がガベコレの対象になるように、ガベコレ直前に`q = null;`を追加してみました。

```csharp:q=nullにしてガベコレ対象にする
        static Queue<string[]> q;
        static void Main(string[] args)
        {
            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            q = CreateQueue();
            q = null;  // ガベコレの対象になるはず
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
```

おお、ガベコレに回収されてる。やったね。

```csharp:実行結果（リリースモード）
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :40124
```
# static変数の場合（Dequeue()でキューを空にする）
次は`null`を代入する代わりに、キューの全要素を`Dequeue()`で空にしてみます。

```csharp:Dequeue()でキューを空にしてガベコレ
        static Queue<string[]> q;
        static void Main(string[] args)
        {
            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");

            var slotCount = q.Count;
            for (int i = 0; i < slotCount; ++i)
                q.Dequeue();

            Console.WriteLine($"3. After Dequeue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            Console.WriteLine($"4. After GC.Collect()   :{GC.GetTotalMemory(false)}");
            q = null;
            GC.Collect();
            Console.WriteLine($"5. After set null to q  :{GC.GetTotalMemory(false)}");
        }
```

解放されますね。要素数が大きいときは、`Queue.TrimExcess()`で空きスロットをshrinkすれば、よりオーバーヘッドが減ると思います。

```:実行結果（リリースモード）
The maximum generation is 2.
1. Before CreateQueue() :64584
2. After CreateQueue()  :8219792
3. After Dequeue()  :8219792
4. After GC.Collect()   :65048
5. After set null to q  :64728
```

# static変数の場合（Clear()でキューを空にする）

```csharp:Clear()でキューを空にする
        static Queue<string[]> q;
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");

            q.Clear();

            GC.Collect();
            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
```

`Clear()`でも同じく、ガベコレに回収されるですね。

```:実行結果（リリースモード）
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :40184
```

# static変数の場合（new演算子で領域を再割り当て）

`q.Clear();`の代わりに、`q = new Queue<string[]>();`に変更したところ、やはりガベコレに回収されました。

```:実行結果（リリースモード）
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :40156
```

`new`による再割当て前の領域はダングリング参照とはならずに、ちゃんとガベコレに回収されるんですね（それがガベコレの仕事なのだが…）。

#static変数の場合（クラスのstaicメンバ）

キュー（static領域）をメンバにもつクラスのインスタンス変数に`null`を代入したケースです。

```csharp:クラスのstaticメンバ（インスタンスは解放）
class QueueTest
{
    static public Queue<string[]> q;
}

namespace GCTest
{
    class Program
    {
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            var aQueueTest = new QueueTest();
            QueueTest.q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");

            aQueueTest = null;
            GC.Collect();

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
    // ...
```

インスタンスに`null`を代入しても、メンバが参照するリソースは`static`領域にあるため、リソースはガベコレに回収されませんね。

```:実行結果（リリースモード）（キューをメンバにもつクラスのインスタンスにnullを代入）
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue()  :8219792
3. After GC.Collect()   :8224688
```

staticメンバにnullを代入して`static`領域のリソースへの参照を断つと、やっとガベコレに回収されました。

```csharp:staticメンバにnullを代入
            // aQueueTest = null;
            QueueTest.q = null;
            GC.Collect();
```

```:実行結果（リリースモード）（staticメンバにnullを代入）
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue()  :8219792
3. After GC.Collect()   :64752
```

#おわりに
くぅ疲です。「ガベージ・コレクタ」というと、高度なアルゴリズムで武装された、頭の良い人だけが意識する近代兵器というイメージがあったのですが、こうやって実コードで動作を確認してみると、意外と身近なものに思えてくるものですね。筆者はガベコレのことが、ちょっと好きになりました。