CLRのガベージコレクション
「プログラミング .NET Framework 第2版」片手に、ガベージコレクションについて。
ジェネレーション
CLRのガベージコレクションはジェネレーションで管理される。
ジェネレーションは以下三つの考え方に基づいている
- オブジェクトが新しいほど、その寿命は短い
- オブジェクトが古いほど、その寿命は長い
- 一部のヒープに対してガベージコレクションした方が、効率が良い
CLRのジェネレーション
- ジェネレーションは0・1・2の三つ
- 各ジェネレーションには予算が割り振られている(ジェネレーション0の予算は256kb 等)
- 最初は全てジェネレーション0
- ただし85,000byte以上のあらゆるオブジェクトは「ラージオブジェクト」としてLarge Object Heapに配置される
- 「ラージオブジェクト」は常にコンパクト化の対象外で、かつ最初からジェネレーション2と見なされる
- (このため、大きく短命なオブジェクトを確保しているとパフォーマンスに影響が出る)
- ただし85,000byte以上のあらゆるオブジェクトは「ラージオブジェクト」としてLarge Object Heapに配置される
- ジェネレーション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さんのツッコミにある「ラージオブジェクト」の扱いについて追記しました。