This is CLR - GC (Section 1)
GC の動作は単純明快。「ルート(オブジェクトを参照している変数)の存在しないオブジェクトを回収し、メモリを解放する」ただそれだけである。殆どにおいて、プログラム、或いはプログラマからは GC は全く透過的だ。GC の動作の詳細を気にする必要はない。だが、GC について知識を持っていると役に立つ事がある。パフォーマンス、アンマネージリソースの確実な解放、アンマネージコードとの連携、ルートがないように見えるオブジェクトの維持、ルートがあるように見えるオブジェクトの GC 回収、etc。知識は無用なトラブルを回避してくれる。
アンマネージリソースを内部リソースとして持つクラスは Finalize を必ず実装しなければならない。更に、Dispose パターンを使って、任意のタイミングでアンマネージリソースを解放できるようにすれば尚良しだ。しかし、Dispose の実装は「できればしたほうがよい」レベルだが(※1)、 Finalize の実装は「必ずしなければならない」レベルである。そうでなければ、アンマネージリソースは Windows プロセスが終わるか AppDomain が Unload されるまでキープされる(※2)事になる。だが、Finalize を実装すれば必ずアンマネージリソースは解放されるという事でもない。例えば、Finalize を呼び出す際に CLR は Finalize メソッドを Jit compile するが、その際に Jit compile 後のネイティブコードを格納する場所がないぐらいメモリが逼迫していたらどうなるだろうか。Finalize メソッドが実行されないでアンマネージリソースがリークしてしまうのである(※3)。
では、絶対確実に Finalize が呼ばれるようにするにはどうしたらよいのか。CriticalFinalizerObject を使えばよい。.NET 2.0 から追加されたこのクラスを、Finalize を絶対確実に呼んで欲しいクラスが継承する。CLR は CriticalFinalizerObject から派生しているクラスを特別扱いする。対象クラスのオブジェクトがインスタンス化されると同時に Finalize メソッドもコンパイルしてしまう(※4)。こうする事によって、Finalize メソッドがコンパイルされていないという状況は存在しない。更に、Finalize 呼び出し順序にも影響を与える。前回は、Finalize の呼び出し順に何のルールも存在しないと説明したが、CLR は通常の Finalize を実装しているオブジェクトの Finalize を呼び出した後に CriticalFinalizerObject の Finalize を実装しているオブジェクトの Finalize を呼び出す。こうする事によって、通常の Finalize メソッド内からクリティカルオブジェクトを参照する事ができるのである。
実際には、CriticalFinalizerObject から直接派生したクラスを作成する事は殆どないだろう。継承モデルは使いやすいとはいえない。.NET Framework Class Library の SafeHandle クラスや CriticalHandle クラスを使用する事になる。これらは CriticalFinalizerObject を継承している。Thread クラスも CriticalFinalizerObject だ。
信頼性の高いシステムを構築するには、CER: Constrained Execution Region「制約された実行領域」を参考の事。SQL Server のようなクリティカルなシステムでは、リソースリークを回避する事は非常に大切な事となる(※5)。
※1 設計思想としては、必ず実装した方が良いと言うべきだが。
※2 より明確な言い方はリーク。
※3 ただ、そのようにメモリが逼迫した状況であった場合、何らかの対処が必要となるだろう。
※4 もはやジャストインタイムとは呼べないか?
※5 SQL Server はおいそれとはプロセスのリサイクルはできない。