5. CLR の機能

更新日: 2009 年 10 月 23 日


.NET のアプリケーションは、それが利用しているクラス ライブラリのクラスなども含めて、最終的にはすべてネイティブ コードに変換されますが、そのコードは .NET アプリケーションのランタイム ライブラリである CLR の機能を利用しながら実行されます。例えばプログラムで配列を作成した場合には、それに必要なメモリ領域の割り当てが必要となりますが、配列クラスは OS から直接メモリを確保することはできず、CLR を経由してそれを行います。

このように、.NET アプリケーションの実行する上で必要な中核的な機能は、すべて CLR で実装されています。ここでは CLR の主要な機能である例外処理とメモリ管理について解説していきます。

例外処理

例外処理は、CLR に組み込まれた .NET Framework の基本的な機能の 1 つです。例外とは、その名の通りプログラム上で想定外の操作を行ったときにプログラムの実行を中断する仕組みのことを指します。例えば、次のようなプログラムを実行すると、割り算を実施した個所で ZeroDividedException という例外が発生します。

try
{
  int i = 0;
  int n = 10 / i; // 例外が発生
}
catch (DivideByZeroException ex)
{
   Log.Write(ex);
}

ただし、このプログラムでは例外を catch ブロックにより処理していますので、プログラムは異常終了することなく継続して実行されます

例外の仕組みがあることで、プログラムは基本的に正常な流れだけを記述することができます。例外処理の機構がないプログラミング言語では、コード中の各所に、エラー チェックや値の妥当性チェックなどの正常な動作以外の処理について大量に記述する必要がありました。例外処理がある場合でも、想定できるエラー値についてはチェックする必要がありますが、本来入力されるはずのない値などの例外的な状況に対しては、例外処理を利用してプログラムで集中的に管理できます。

ただし、例外がシステムの環境不備やメモリ不足などの理由で発生した場合、処理が継続して正常に動作するとは限らず、場合によっては不整合が発生してデータの復旧処理などが必要になることも考えられます。従って、例外は復旧可能なものに限ってプログラムで処理するようにし、それ以外の例外は処理しないようにする (わざとプログラムに例外による異常終了をさせる) ことをおすすめします。

上記のコードのように、例外が発生したときには、例外から復旧させるためのコードや、例外の発生状況をログ出力するコードを catch ブロックに記述します。また、例外が発生したときにも、発生していないときにも同じ処理を実行したい場合があります。例えば、データベースとの接続やファイルなど、処理が完了したらすぐに解放すべきりソースを使用している場合には、正常に処理が完了したときも、エラーが発生して処理が中断したときにも、同じように解放する必要があります。このように正常時にも異常時にも必ず実行される処理ブロックとして、finally ブロックを記述することができます。

SqlConnection connection
Try
{
  connection = new SqlConnection(); // データベースのコネクション
  // コネクションをオープンして、
  // データベースへのアクセス処理を行う
}
catch (Exception ex)
{
  // 復帰するためのコードやログ出力を行う
}
finally
{
  // 成功時にも失敗時にも実行される処理を記述する。
  connection.Close(); // コネクションのクローズ
}

例外オブジェクト (Exception クラスのオブジェクト) には、例外が発生した原因を特定するための詳細な情報が記述されています。例えば、スタック トレースと呼ばれる情報がそれに当たります。スタック トレースは、例外オブジェクトの StackTrace プロパティによって公開されていますが、例外が発生した処理の呼び出し履歴を取得できるようになっています。

次の情報は例外が発生した場合に出力される、テキスト化された例外オブジェクトの内容ですが、App クラスの Method2 メソッドの中で InvalidOperationException 例外が発生していることを示しています。このメソッドの呼び出し元が App クラスの Main メソッドであることが分かりますが、詳細な情報が省略されています。

ハンドルされていない例外: System.InvalidOperationException: オブジェクトの現在の状態に問題があるため、操作は有効ではありません。
   場所 App.Method2()
   場所 App.Main()

デバッグ時 (デバッグ ビルドを行っている場合) には、より詳細な情報が取得できます。

ハンドルされていない例外: System.InvalidOperationException: オブジェクトの現在の状態に問題があるため、操作は有効ではありません。
   場所 App.Method2() 場所 c:\lib\throwException.cs:行 17
   場所 App.Method1() 場所 c:\lib\throwException.cs:行 12
   場所 App.Main() 場所 c:\lib\throwException.cs:行 7

これを見ると、Main メソッドから Method1 メソッドが呼ばれ、その中から Method2 メソッドが呼ばれていることが分かります。さらに例外の発生個所がソース コード上の何行目なのかという詳細情報まで分かります。このように例外の詳細情報が取得できることで、容易に問題の特定や分析を行えるようになります。

.NET Framework で提供されている例外に適切な例外がない場合や、より詳細な例外情報を返したい場合は、例外クラスを独自に定義できます。実装方法は単純に例外クラスを継承するだけです。

public class CustomException : Exception
{
  // 独自の例外の実装
}

ここでは、例外の最も基本的なクラスである Exception クラスを継承しています。

プログラムから意図的に例外を発生させる (スローする) こともできます。プログラム内で継続不可能な問題が発生した場合などに、これを行います。

public void Method2()
{
  if (継続不可能な問題が発生)
  {
    throw new InvalidOperationException("問題の説明");
  }
}

ただし、例外のスローは、スタック トレース情報の収集などを行うため、パフォーマンスに影響を与える可能性があります。また、必ずしも呼び出し元で例外を正しく処理できるとは限らないため、実際に処理の継続が不可能な場合のみに例外をスローし、復帰可能な場合にはメソッドの戻り値などで処理すべきです。

ページのトップへ


自動メモリ管理

.NET では、メモリ管理が自動化されています。.NET 以前には、メモリの明示的な確保と解放が必要でしたが、プログラマーによる解放のし忘れによって、メモリリークや外部コンポーネントがメモリから解放されないといった問題がしばしば発生していました。

.NET ではこのような問題を回避するために、メモリの確保、解放に新しい仕組みが採用されており、CLR がそれをすべて管理します。これにより、プログラムでは基本的にメモリの管理を行う必要がありません。以下ではその仕組みを説明します。

CLR が管理するメモリ空間は、管理された領域という意味でマネージ ヒープと呼ばれています。プログラムが実行されると、CLR は OS のメモリ領域から連続した領域をマネージド ヒープとして確保します。そして、プログラムがオブジェクトをインスタンス化するたびに、マネージ ヒープから必要なメモリ領域を割り当てます。以降の図で NextObjPtr は、次に確保可能な領域を示す CLR が管理しているポインタです。

以降の図で NextObjPtr は、次に確保可能な領域を示す CLR が管理しているポインタです。

プログラムでは、メモリの明示的な解放を行わないため、アプリケーションを実行しているうちにマネージ ヒープ領域は不足していきます。

プログラムでは、メモリの明示的な解放を行わないため、アプリケーションを実行しているうちにマネージ ヒープ領域は不足していきます。

新しくオブジェクトをインスタンス化しようとしたときにマネージ ヒープ領域が不足すると、 CLR は ガベージ コレクション (以下、GC) を実行して、不要になっているオブジェクトの領域を解放して回収します。

GC の仕組みは次のようになっています。まず、一度すべてのオブジェクトを解放可能であるとしてマークし、次に現在使用されているオブジェクトのマークを外していきます。これはルート オブジェクトと呼ばれるオブジェクトからマークを外し、次にルート オブジェクトから参照されているオブジェクトのマークを外していきます。このような作業を行うことによって、使用されていないオブジェクトにのみマークが残ることになり、それらが回収の対象になります。

このような作業を行うことによって、使用されていないオブジェクトにのみマークが残ることになり、それらが回収の対象になります。

ガベージ コレクションによって回収されたメモリ領域は空き領域となりますが、.NET ではメモリのデフラグを避けるためにコンパクションという操作を行い、メモリ領域の再配置を行います。

ガベージ コレクションによって回収されたメモリ領域は空き領域となりますが、.NET ではメモリのデフラグを避けるためにコンパクションという操作を行い、メモリ領域の再配置を行います。

GC の基本的な動作について解説しましたが、特筆すべき特徴としては、世代管理と呼ばれるメモリの管理手法が挙げられます。

.NET では、「ほとんどの小さなオブジェクトは短時間で確保と解放が繰り返されるが、それ以外のオブジェクトは長く使用される」という研究成果に基づいて、オブジェクトの保持期間を世代で管理します。つまり、GC によって解放されなかったオブジェクトは、長く使われる可能性があるオブジェクトとして世代を進めて、頻繁に GC が実施されないようにします。こうすることによって、GC のパフォーマンスを向上させ、短期間で解放されるオブジェクトを効率よく解放することができるようになっています。

世代は、Gen (Generation の略) という名前で管理されていて、新しいものから Gen 0、Gen 1 とカウントアップされていき、Gen 2 が最も古い世代になります。先ほどの GC の例では、解放されなかったオブジェクトは、Gen 1 になり、Gen 0 のオブジェクトと比較して、解放されにくいオブジェクトになります。

先ほどの GC の例では、解放されなかったオブジェクトは、Gen 1 になり、Gen 0 のオブジェクトと比較して、解放されにくいオブジェクトになります

その後、Gen 0 の GC は、頻繁に実行されますが、Gen 1 は、CLR が算出したしきい値を超えるまで GC が実行されず、解放されるタイミングは遅延されます。Gen 1 でも解放されなかったオブジェクトは、Gen 2 に移行し、それよりもさらに解放されにくくなります。

さらにもう 1 つ特筆するポイントがあります。.NET では、ファイルやデータベース接続などの数に限りがあるリソースを確実に解放するために Dispose メソッドと Finalize メソッドが提供されています。通常は、Dispose メソッドを呼び出すことによってリソースを解放しますが、明示的な Dispose メソッドの呼び出しが実施されなかった場合に CLR が Finalize メソッドを呼び出して確実に解放を実施します。Dispose メソッドと Finalize メソッドは、同じ処理を実行しますが、Dispose メソッドは開発者がコードから呼び出すのに対して、Finalize メソッドは Dispose が実施されなかったオブジェクトに対して CLR が呼び出すメソッドになります。

Finalize メソッドを持っているオブジェクトがインスタンス化されると、ファイナライゼーション リストと呼ばれる内部のリストに参照が追加されます。

Finalize メソッドを持っているオブジェクトがインスタンス化されると、ファイナライゼーション リストと呼ばれる内部のリストに参照が追加されます。

GC が実行されて不要なオブジェクトが解放されるとき、ファイナライゼーション リストに存在しているオブジェクトは、F リーチャブル キューと呼ばれるリストに追加されます。F リーチャブル キューは、Finalize メソッドの呼び出しで GC の処理が遅くなることを避けるために Finalize メソッドを非同期で呼び出すためのキューになります。オブジェクトの参照が F リーチャブル キューに追加されると、元のオブジェクトは、Gen 1 で維持されて、Finalize メソッドの呼び出しを待つことになります。

オブジェクトの参照が F リーチャブル キューに追加されると、元のオブジェクトは、Gen 1 で維持されて、Finalize メソッドの呼び出しを待つことになります。

Gen 1 で GC が実行されるのは、Gen 0 がしきい値を超えたメモリを確保しようとしたときに、Gen 1 もしきい値を超えたときであるため、このオブジェクトが解放されるのは大幅に遅延されることになります。

以上のように余分な処理やメモリ領域が必要となるため、可能な限り Finalize メソッドが呼び出されないようにすべきです。そして、Finalize メソッドが呼び出されないようにするには、コードから確実にDispose メソッドを呼び出すことです。Dispose メソッドでは、以下のような実装が一般的です。

public void Dispose()
{
  // アンマネージ リソースを解放する。
  GC.SuppressFinalize(this);
}

最後の行で実行している GC.SuppressFinalize メソッドの呼び出しは、GC に対して、Dispose メソッドによってリソースは解放済みなので、Finalize メソッドの呼び出しは不要であることを宣言しています。このように Dispose メソッドを持つオブジェクトは、不要になった時点で Dispose メソッドを呼び出すことで、オブジェクトがメモリ上に長期間残ってしまうのを避けることができます。

ページのトップへ