CLRのガベージコレクション

「プログラミング .NET Framework 第2版」片手に、ガベージコレクションについて。

ジェネレーション

CLRガベージコレクションはジェネレーションで管理される。
ジェネレーションは以下三つの考え方に基づいている

  • オブジェクトが新しいほど、その寿命は短い
  • オブジェクトが古いほど、その寿命は長い
  • 一部のヒープに対してガベージコレクションした方が、効率が良い

CLRのジェネレーション

  • ジェネレーションは0・1・2の三つ
  • 各ジェネレーションには予算が割り振られている(ジェネレーション0の予算は256kb 等)
  • 最初は全てジェネレーション0
    • ただし85,000byte以上のあらゆるオブジェクトは「ラージオブジェクト」としてLarge Object Heapに配置される
      • 「ラージオブジェクト」は常にコンパクト化の対象外で、かつ最初からジェネレーション2と見なされる
      • (このため、大きく短命なオブジェクトを確保しているとパフォーマンスに影響が出る)
  • ジェネレーション0の予算を使い切ったらガベージコレクション実施、オブジェクトは破棄される
  • 生き残ったオブジェクトはジェネレーション1となる
  • 次のガベージコレクションは、ジェネレーション1の予算を使い切っていない限り、ジェネレーション0のみが対象
    • ジェネレーション1に不要なオブジェクトが残っていても無視される
  • ジェネレーション1の予算を使い切ったときはじめて、ジェネレーション1がガベージコレクトの対象となる
    • ここで生き残ったオブジェクトはジェネレーション2となる
  • ジェネレーション2も、予算を使い果たすまでガベージコレクト対象とならないのはジェネレーション1と同じ
  • 0<1<2の順で予算は多めに設定される
  • オブジェクトの利用方法などを観察し、学習する(オブジェクトの扱われ方を見て予算を動的に変更する)

GCの様子を見る

上記で知ったGCの動作を見るために、サンプルを書いてみる。

    internal class Foo
    {
        internal static readonly Int32 MAX_INDEX = 99999;
        private string name_;
        private Int32[] fields_;
        internal Foo(string name)
        {
            name_ = name;
            fields_ = new Int32[MAX_INDEX + 1];

            Random r = new Random(DateTime.Now.Second);
            for (int i = 0; i < fields_.Length; i++)
            {
                fields_[i] = r.Next();
            }
        }
        ~Foo()
        {
            System.Diagnostics.Debug.WriteLine(string.Format("finalize:{0}", name_));
        }
        internal int this[Int32 index]
        {
            get
            {
                return fields_[index];
            }
        }
    }

こんなクラスがあって、

    class Program
    {
        static void Main(string[] args)
        {
            DateTime st = DateTime.Now;
            CallFoo("A");
            CallFoo("B");
            CallFoo("C");
            DateTime ed = DateTime.Now;
            System.Diagnostics.Debug.WriteLine(string.Format("time:{0}",ed - st));
        }
        static void CallFoo(string perfix)
        {
            Random r = new Random(DateTime.Now.Second);
            Foo[] fs = new Foo[3];
            for (int i = 0; i < fs.Length; i++)
            {
                fs[i] = new Foo(perfix + i.ToString());
            }
            foreach (Foo f in fs)
            {
                Console.WriteLine(f[r.Next(0, Foo.MAX_INDEX)]);
            }
            System.Diagnostics.Debug.WriteLine(string.Format("@CallFoo:{0}", perfix));
        }
    }

こんな風に実行した場合、ログは、

@CallFoo:A
@CallFoo:B
finalize:B1
finalize:A0
finalize:B0
finalize:B2
finalize:A2
@CallFoo:C
finalize:A1
time:00:00:00.0468750
finalize:C0
finalize:C2
finalize:C1

こんな感じ。finalizeはリソース解放時に呼び出される。
CallFoo("B")呼び出しが終わった段階でCallFoo("A")・CallFoo("B")で確保されたオブジェクトがGCの対象になってる。
Fooの内部配列のサイズを小さくしてやる。

        internal static readonly Int32 MAX_INDEX = 10;

そうすると、

@CallFoo:A
@CallFoo:B
@CallFoo:C
time:00:00:00.0156250
finalize:C2
finalize:C1
finalize:C0
finalize:B2
finalize:B1
finalize:B0
finalize:A2
finalize:A1
finalize:A0

finalizeはCallFoo("C")の後に呼び出されている。

CallFooの最後に

            GC.Collect();

を呼び出してみる。MAX_INDEX = 99999の場合。

@CallFoo:A
@CallFoo:B
finalize:A2
finalize:A1
finalize:A0
@CallFoo:C
finalize:B2
time:00:00:00.0468750
finalize:B1
finalize:B0
finalize:C0
finalize:C2
finalize:C1

明示的に呼び出しても、結果としてリソース解放のタイミングははあまり変わらない。

追記(2008/11/16)

Panさんのツッコミにある「ラージオブジェクト」の扱いについて追記しました。