ガベージコレクタ

実はあまりよく調べたことがないので、復習を兼ねて。

GCが動作するタイミングは、

  1. Generation0がいっぱいになる
  2. GC.Collectの明示的な呼び出し
  3. OSからのメモリ不足通知
  4. AppDomainがアンロード
  5. CLRが終了

というケースがありますが、その中の1についてみてみます。

using System;
using System.Threading;

class Program
{
    static void Main(string args)
    {
        // 100ms毎にHelloを表示
        new Timer(delegate { Console.WriteLine("Hello"); }, null, 0, 100);

        // メモリを確保しまくる
        // Generation0がメモリ不足になるとGCが走る
        // するとどこからも参照されていないTimerオブジェクトが死ぬ
        object o = new byte[80*1024];
        Console.WriteLine("o is generation {0}", GC.GetGeneration(o));
        byte b = null;
        for (int i = 0; i < 15; ++i)
        {
            Thread.Sleep(200);
            // 85kb以上はラージヒープ行きなので、それ以下のサイズに。
            b = new byte[85 * 1000 - 12 -1];
            Console.WriteLine(GC.GetTotalMemory(false));
        }
        byte[] c = b;
        Console.WriteLine("o is generation {0}", GC.GetGeneration(o));

        Console.ReadLine();
    }
}

/* 結果
o is generation 0
Hello
Hello
320520
Hello
Hello
...
...
1170640
Hello
1255652
Hello
Hello
312620
405824
490836
o is generation 1
 */

結果に注目すると、最後の方で使用中のメモリが1255652から312620に減少し、オブジェクトoがGeneration1になっています。また、Helloが表示されていないことから、どこからも参照されていなかったTimerオブジェクトがゴミ集めされてしまったことが分かります。
あと、本題とは違いますが、例のようなTimerの使い方をするとGCに回収されてしまいますので、回収されたくない場合はGC.KeepAliveなどを使ってオブジェクトの参照を保持するようにしなければなりません。

ちなみにコメントにあるように85kb以上のオブジェクトはラージヒープという別の領域に確保されます。これは、ガベージコレクト時にメモリの解放だけでなく、オブジェクトが削除されたことで虫食いになった領域を詰めて連続した空き領域を作る処理(コンパクション)を行いますが、これを大きいオブジェクトに対して行うとパフォーマンスに悪影響を与えるため、ラージヒープという別の領域を用意し、その領域はコンパクションを行わないようになっています。

ところで、ラージヒープに確保されたオブジェクトのGenerationは何だと思います?

using System;

class Program
{
    static void Main()
    {
        byte b1 = new byte[85 * 1000 - 12];
        Console.WriteLine("b1 is generation {0}", GC.GetGeneration(b1));
        byte b2 = new byte[85 * 1000 - 12 -1];
        Console.WriteLine("b2 is generation {0}", GC.GetGeneration(b2));
    }
}

/* 結果
b1 is generation 2
b2 is generation 0
 */

このようにGeneration2扱いだったりします。