Microsoft .NET のすべて

ガベージコレクション入門: Microsoft .NET Framework の自動メモリ管理 Part II

Garbage Collection, Part 2: Automatic Memory Management in the Microsoft .NET Framework
(December 2000 Vol.15 No.12)

Jeffrey Richter

Jeffrey Richter は、『Programming Application for Microsoft Windows』 (Microsoft Press刊、1999 年) の著者で、ソフトウェアコンサルティングおよび教育の会社Wintellect(http://www.Wintellect.com Leave MS Site)の共同設立者でもある。Microsoft .NETプラットフォームとWin32をターゲットとするシステムの設計/プログラミングを専門としている。現在、JeffreyはMicrosoft .NET Frameworkプログラミングのための書籍を執筆し、.NET テクノロジーのセミナーで教えている。

この記事は、C および C++ についての知識があることを前提に書かれています。

関連情報については、本誌ガベージコレクション入門「Microsoft .NET Frameworkの自動メモリ管理 Part I」を参照すること。

背景となる情報については、Richard Jones、Rafael Lins共著『Garbage Collection: Algorithms for Automatic Dynamic Memory Management』(John Wiley & Son刊, 1996年)、およびJeffrey Richter著『Programming Applications for Microsoft Windows』(Microsoft Press刊, 1999年)を参照すること。

このアーティクルは、株式会社アスキー Leave MS Site より 2000 年 12 月 18 日に発刊された MSDN Magazine Leave MS Site No.10 (ISBN4-7561-3676-1) に収録されています。



Part I では、ガベージコレクションをサポートする環境が必要とされる理由と、CLRが使っているアルゴリズムとその内部動作についても説明した。その他、リソースの確保方法、自動ガベージコレクションの仕組み、完結化機能を使ったオブジェクトのクリーンアップ、復活によるオブジェクトへのアクセスの回復、その背景について説明した。

Part II では、オブジェクトに対する強弱2種類の参照の実装の様子、オブジェクトをジェネレーションに分類して処理性能を向上させる仕組み、Ssytem.GC型を使ってガベージコレクションを直接操作する方法を説明する。また、マルチスレッドアプリケーションの処理性能を向上させるためのメカニズムや処理性能の監視方法についても説明する。

Part I ~ II で、ガベージコレクションについての話題は、ほぼ網羅することができた。これらの知識があれば、アプリケーションのメモリ管理を単純化し、処理性能を向上させることができるはずである。



Summary

この2回連載の第1部では、ガベージコレクションアルゴリズムの仕組み、ガベージコレクタがリソースのメモリを解放することにしたときにリソースを適切にクリーンアップするための方法、解放されたときにオブジェクトにクリーンアップを強制するための方法を説明した。この第2部では、大規模オブジェクトのメモリ管理を助ける参照の強弱、オブジェクトのジェネレーションとそれが処理性能を上げる仕組みを説明する。またガベージコレクションを制御するメソッドとプロパティの使い方、ガベージコレクションの処理性能を監視するための方法、マルチスレッドアプリケーションにおけるガベージコレクションについて説明する。



Part I では、ガベージコレクションをサポートする環境が必要とされる理由を説明した。すなわち、メモリ管理の単純化ということである。また、Common Language Runtime (CLR:言語共通ランタイム) が使っているアルゴリズムとその内部動作についても説明した。また、依然としてリソース管理とクリーンアップを明示的に処理しなければならない場合、Finalize、Close、Disposeメソッドを実装する方法も示した。今回は、CLRガベージコレクタの議論を締めくくる。

まず、大型のオブジェクトが管理ヒープにセットされたときのメモリの負担を軽減するために使われる、弱参照 (weak reference) という機能を説明する。次に、ジェネレーションによる処理性能の向上について説明する。最後に、マルチスレッドのガベージコレクションや、ガベージコレクタのリアルタイムの動作を監視するためにCLRが提示しているパフォーマンスカウンタなど、ガベージコレクタが提供する処理性能の向上について解説して締めくくる。


弱参照 (weak reference)

ルートがオブジェクトを指すときには、アプリケーションコードがオブジェクトにたどり着けるということなので、ガベージコレクタはオブジェクトを回収できない。ルートがオブジェクトを指すときには、オブジェクトに対する強参照 (strong reference) と表現する。しかし、ガベージコレクタは、弱参照もサポートする。弱参照が存在する状態では、ガベージコレクタがオブジェクトを回収できる一方で、アプリケーションもオブジェクトにアクセスできる。どうしたらそのようなことが可能になるのだろうか。すべてはタイミングの問題である。

オブジェクトに弱参照しかない状態で、ガベージコレクタが実行されると、オブジェクトは回収され、あとでアプリケーションがオブジェクトにアクセスしようとしても、アクセスは失敗する。一方、弱参照を持つオブジェクトにアクセスするためには、アプリケーションはオブジェクトに対する強参照を取得しなければならない。ガベージコレクタがオブジェクトを回収する前にアプリケーションが強参照を獲得すると、強参照が存在するのでガベージコレクタはオブジェクトを回収できなくなる。以上の話は、十分分かりにくいと思うので、Figure 1 のコードを使って明確にしていこう。


Void Method() {
   Object o = new Object();    // Creates a strong reference to the
                               // object.

   // Create a strong reference to a short WeakReference object.
   // The WeakReference object tracks the Object.
   WeakReference wr = new WeakReference(o);

   o = null;    // Remove the strong reference to the object

   o = wr.Target;
   if (o == null) {
      // A GC occurred and Object was reclaimed.
   } else {
      // a GC did not occur and we can successfully access the Object
      // using o
   }
}

 Figure 1 強参照と弱参照

なぜ弱参照を使うのだろうか。簡単に作成できるが大量のメモリを必要とするデータ構造が使われているときには、弱参照が役に立つ。たとえば、ユーザーのハードディスクに含まれるすべてのディレクトリとファイルを把握していなければならないアプリケーションがあったとする。この情報を反映した木構造は簡単に作成できる。そして、アプリケーション実行中にユーザーのハードディスクに実際にアクセスする代わりにメモリ内の木構造を参照すれば、アプリケーションの処理性能を大幅に向上させられる。

問題は、木構造が非常に大規模なものになって、メモリを大量に消費する可能性があるということである。ユーザーがアプリケーションの別の部分にアクセスし始めると、木構造は不要になって、ただ貴重なメモリを浪費するだけになっているかもしれない。木構造を削除してもかまわないが、ユーザーがアプリケーションの最初の部分に戻ってきたら、木構造を再び作らなければならない。弱参照を使えば、簡単かつ効率的にこのような状況に対処できる。

ユーザーがアプリケーションの最初の部分から離れたら、プログラムは木構造に対して弱参照を作成し、すべての強参照を破棄する。アプリケーションのほかの部分のメモリに対する負荷が低ければ、ガベージコレクタは木構造オブジェクトを回収しないだろう。ユーザーがアプリケーションの最初の部分に戻ってきたら、アプリケーションは木構造に対して強参照を獲得しようとする。成功したら、アプリケーションは、ユーザーのハードディスクをたどり直す必要はない。

WeakReference型は、2種類のコンストラクタを持っている。

WeakReference(Object target);
WeakReference(Object target, Boolean trackResurrection);

target引数は、WeakReferenceオブジェクトが管理すべきオブジェクトを識別する。trackResurrection引数は、Finalizeメソッドが呼び出されたあとのオブジェクトをWeakReferenceオブジェクトが管理すべきかどうかを指定する。通常は、trackResurrection引数にはfalseを渡し、第1コンストラクタを使って復活を管理しないWeakReferenceを作成する (復活については、Part I を参照していただきたい)。

便宜上、復活を管理しない弱参照を短い弱参照、管理する弱参照を長い弱参照と呼ぶ。Finalizeメソッドを持たないオブジェクトでは、短い弱参照と長い弱参照の動作は同じである。長い弱参照を使うのは避けたほうがよい。長い弱参照を使うと、Finalizeメソッドが呼び出されて状態が予測不能になったオブジェクトを復活させられるようになってしまう。

オブジェクトに対して弱参照を作成したら、通常はオブジェクトに対する強参照にnullをセットする。強参照が残っていたら、ガベージコレクタはオブジェクトを回収できなくなってしまう。

オブジェクトを再び使うときには、弱参照を強参照に変えなければならない。そのためには、WeakReferenceオブジェクトのTargetプロパティを呼び出し、結果をアプリケーションのルートのどれかに代入すればよい。Targetプロパティがnullを返したら、オブジェクトは回収済みである。Targetがnull以外の値を返したら、ルートはオブジェクトに対する強参照になっており、コードはオブジェクトを操作できる。強参照を持っている限り、オブジェクトは回収されない。


弱参照の内部構造

WeakRerferenceオブジェクトがほかのオブジェクト型とは異なる動作をすることは、前節で説明したことからも明らかだろう。通常、アプリケーションがオブジェクトを参照するルートを持っており、そのオブジェクトがほかのオブジェクトを参照していたら、どちらのオブジェクトも到達可能であり、ガベージコレクタはこれらのオブジェクトが使っているメモリを回収できない。しかし、アプリケーションがWeakReferenceオブジェクトを参照するルートを持つ場合、WeakReferenceオブジェクトが参照しているオブジェクトは到達可能ではないとみなされ、回収可能になる。

弱参照の仕組みを完全に理解するために、管理ヒープの内部を再び見てみることにしよう。管理ヒープには、弱参照の管理だけを目的とする2つの内部データ構造が含まれている。すなわち、短い弱参照テーブルと長い弱参照テーブルである。これら2つのテーブルには、単純に、管理ヒープ内にメモリを割り当てられたオブジェクトへのポインタが含まれている。

初期状態では、2つのテーブルはともに空である。WeakReferenceオブジェクトを作成したとき、オブジェクトは管理ヒープから割り当てられるわけではない。どちらかの弱参照テーブルから空のスロットを探すのである。短い弱参照は短い弱参照テーブルを使い、長い弱参照は長い弱参照テーブルを使う。

空スロットが見つかったら、その値として、管理したいオブジェクトのアドレスをセットする。そしてオブジェクトポインタは、WeakReferenceコンストラクタに渡される。new演算子が返す値は、弱参照テーブル内のスロットのアドレスである。当然ながら、2つの弱参照テーブルはアプリケーションのルートの1つとはみなされない。そうでなければ、ガベージコレクタは弱参照テーブルが指しているオブジェクトを回収できなくなってしまう。

ガベージコレクタ (GC) が実行されたときに起きることは、次のとおりである。

  1. ガベージコレクタが、すべての到達可能オブジェクトのグラフを組み立てる。このときの動作はPart I に述べた。
  2. ガベージコレクタが短い弱参照テーブルを走査する。テーブル内のポインタがグラフに含まれていないオブジェクトを参照していたら、そのポインタは到達不能オブジェクトを指しているので、スロットにはnullがセットされる。
  3. ガベージコレクタが完結化キューを走査する。キュー内のポインタがグラフに含まれていないオブジェクトを参照していたら、そのポインタは到達不能オブジェクトを指しているので、オブジェクトは完結化キューからFリーチャブルキューに移される。オブジェクトは到達可能になったとみなされるので、この時点でグラフに追加される。
  4. ガベージコレクタが長い弱参照テーブルを走査する。テーブル内のポインタがグラフに含まれていないオブジェクトを参照していたら (この時点のグラフにはFリーチャブルキュー内のエントリが指しているオブジェクトが含まれている)、そのポインタは到達不能オブジェクトを指しているので、スロットにはnullがセットされる。
  5. ガベージコレクタが、メモリコンパクションによって、到達不能オブジェクトが残した穴を埋める。

ガベージコレクションプロセスのロジックを理解したら、弱参照の仕組みを理解するのも簡単である。WeakReferenceのTargetプロパティにアクセスすると、システムは適切な弱参照テーブルのスロットに含まれている値を返す。スロット内の値がnullなら、オブジェクトは回収済みである。

短い弱参照は、復活を管理しない。つまり、オブジェクトが到達不能だとみなされると、短い弱参照テーブル内のポインタはガベージコレクタによって即座にnullにされる。オブジェクトがFinalizeメソッドを持ち、Finalizeメソッドがまだ呼び出されていなければ、オブジェクトはまだ存在する。アプリケーションがWeakReferencオブジェクトのTargetプロパティにアクセスすると、オブジェクトは実際には存在するにも関わらず、nullが返される。

長い弱参照は、復活を管理する。つまり、オブジェクトのメモリが回収可能になるまで、ガベージコレクタは長い弱参照テーブル内のポインタをnullにしない。オブジェクトがFinalizeメソッドを持つ場合、Finalizeメソッドが呼び出され、オブジェクトが復活不能になるまで、テーブル内のポインタはnullにならない。

ジェネレーション

ガベージコレクタ管理下の環境で仕事を始めた頃、私は処理性能の低下についてかなりの懸念を持っていた。私はC/C++プログラマとして15年以上の経験を持っている。ヒープメモリの確保、解放のオーバーヘッドがどれだけのものかについては十分に理解しているつもりだ。そして、WindowsとCランタイムの各バージョンは、処理性能を改善するために、ヒープアルゴリズムに手を入れてきた。

GCの開発者たちも、Windows、Cランタイムの開発者と同様に、処理性能の改善のためにガベージコレクタに手を入れている。ジェネレーションは、純粋に処理性能の向上のために存在するガベージコレクタの機能の1つである。ジェネレーショナルガベージコレクタ (エフェメラル (短命の) ガベージコレクタとも呼ばれる) は、以下の仮定に基づいて動作する。

  • オブジェクトが新しければ新しいほど、その寿命は短いはずだ。
  • オブジェクトが古ければ古いほど、その寿命は長いはずだ。
  • 新しいオブジェクトは互いに強い関係を持ち、同時に頻繁にアクセスされる。
  • ヒープの一部のコンパクションは、ヒープ全体のコンパクションよりも高速である。

もちろん、多くの既存アプリケーションでこれらの仮定が成り立つことは、さまざまな研究によって実証されている。そこで、ここではこれらの仮定がガベージコレクタの実装に与えた影響について見てみることにしよう。

初期化された時点の管理ヒープは、オブジェクトを持っていない。この時点でヒープに追加されたオブジェクトは、Figure 2 に示すように、ジェネレーション0に所属する。単純化して言えば、ジェネレーション0のオブジェクトは、ガベージコレクタによる検証をまだ受けていない若いオブジェクトである。

Dd297765.Garbage_2_Fig02(ja-jp,MSDN.10).jpg
 Figure 2 ジェネレーション 0

ヒープにさらに多くのオブジェクトが追加されると、ヒープはいっぱいになり、ガベージコレクションが発生する。ガベージコレクタは、ヒープを分析するときに、ごみ (ピンク) のグラフとごみでないオブジェクトのグラフを作る。ガベージコレクションで生き残ったオブジェクトは、ヒープの最左端にまとめられる。これらガベージコレクションで生き残ったオブジェクトは古くなっており、ジェネレーション1に属するものとみなされる (Figure 3)。

Dd297765.Garbage_2_Fig03(ja-jp,MSDN.10).jpg
 Figure 3 ジェネレーション 0 とジェネレーション 1

そのあとにヒープに追加されたオブジェクトは、新しく若いオブジェクトであり、ジェネレーション0に属する。ジェネレーション0が再びいっぱいになると、GCが発生する。今度のGCでも生き残ったジェネレーション1のオブジェクトは、ジェネレーション2としてまとめられる (Figure 4)。ジェネレーション0のオブジェクトのなかでGCを生き残ったものは、ジェネレーション1にまとめられる。GC発生直後は、ジェネレーション0にはオブジェクトはいっさい含まれていないが、新しいオブジェクトはすべてジェネレーション0に配置される。

Dd297765.Garbage_2_Fig04(ja-jp,MSDN.10).jpg
 Figure 4 ジェネレーション 0、1、2

現在のところ、CLRのガベージコレクタがサポートする最高のジェネレーションは、ジェネレーション2である。その後のGCを生き残ったジェネレーション2のオブジェクトは、単純にジェネレーション2に留まる。

ジェネレーショナル GC による処理性能向上

すでに述べたように、ジェネレーショナルなガベージコレクションは、処理性能を向上させる。ヒープがいっぱいになってGCが発生したとき、ガベージコレクタはジェネレーション0のオブジェクトだけを解析し、高いジェネレーションのオブジェクトを無視することができる。結局のところ、新しいオブジェクトほど、寿命は短いだろうと予想されるのである。そこで、ジェネレーション0でオブジェクトを回収し、ジェネレーション0にコンパクションをかければ、かなりの量のスペースをヒープ内に確保でき、すべてのジェネレーションのオブジェクトを回収するよりも高速であろうという訳である。

これは、ジェネレーショナルGCから得られる最も単純な最適化である。ジェネレーショナルコレクタは、管理ヒープ内の一部のオブジェクトを無視することによって、さらに処理性能を向上させられる。古い世代のオブジェクトを参照するルートまたはオブジェクトがある場合、ガベージコレクタはこれら古いオブジェクトの内部参照を無視することができるので、到達可能オブジェクトのグラフを構築するために必要な時間が短縮される。もちろん、古いオブジェクトが新しいオブジェクトを参照している可能性はある。これらの新しいオブジェクトを解析するために、コレクタはシステムのライトウォッチサポート (Kernel32.dllに含まれているWin32 GetWriteWatchファンクション) を利用する。ライトウォッチを使えば、最後のGC以降、書き込みが行なわれている古いオブジェクトが分かる。そのようなオブジェクトに限り、新しいオブジェクトを参照しているかどうかをチェックすればよい。

ジェネレーション0の回収だけでは必要なスペースが得られないときには、コレクタはジェネレーション1とジェネレーション0のオブジェクトを回収する。それでもだめなら、コレクタはすべてのジェネレーション (2、1、0) のオブジェクトを回収する。どのジェネレーションを回収の対象にするかを決めるアルゴリズムの細部は、Microsoftが永遠に操作し続ける領域の1つになるだろう。

ほとんどのヒープ (Cランタイムヒープなど) は、フリースペースを見つけた場所に、オブジェクトを割り当てていく。そのため、連続して複数のオブジェクトを作成したとき、これらのオブジェクトは数MBも離れた位置に配置される可能性がある。しかし、管理ヒープでは、連続して確保されたオブジェクトは、メモリの連続領域に配置されることが保証されている。

先ほど述べた仮定のなかには、新しいオブジェクトは互いに強い関係を持つ傾向があり、同時にアクセスされる可能性が高いというものが含まれていた。新オブジェクトがメモリの隣り合った位置に配置されるので、参照の局所性による処理性能向上が見込める。より具体的に言えば、それらすべてのオブジェクトがCPUのキャッシュに格納される可能性が高い。CPUがキャッシュミスによるRAMアクセスを起こさずに、ほとんどの処理を実行できるようになるので、アプリケーションからのこれらのオブジェクトへのアクセスはかなり高速なものにある。

Microsoftの処理性能テストによれば、管理ヒープ内のメモリ確保は、Win32 HeapAllocファンクションによる標準のメモリ確保よりも高速である。また、200MHz Pentiumでジェネレーション0に完全にGCをかけても1ミリ秒以下の時間しかかからないと示されている。Microsoftの目標は、GCに通常のページフォルトよりも長い時間がかからないようにすることである。

System.GC を使った直接制御

System.GC型を使えば、アプリケーションはガベージコレクタを直接制御できる。まず手始めに、GC.MaxGenerationプロパティを読み出せば、管理ヒープがサポートするジェネレーションの最大値が分かる。現在のところ、GC.MaxGenerationプロパティは必ず2を返す。

次に示す2つのメソッドのうちのどちらかを呼び出せば、ガベージコレクタに強制的にGCを実行させることもできる。

void GC.Collect(Int32 Generation)
void GC.Collect()

第1のメソッドを使えば、GCの対象となるジェネレーションを指定できる。0以上GC.MaxGeneration以下の任意の整数を渡してよい。0を渡すと、ジェネレーション0が回収され、1を渡すとジェネレーション1、ジェネレーション0が回収される。2を渡せば、ジェネレーション2からジェネレーション0までが回収される。引数なしバージョンのCollectは、全ジェネレーションのフルコレクションを強制するもので、次のメソッドを呼び出すのと同じである。

GC.Collect(GC.MaxGeneration);

ほとんどの場合は、どのような形であれ、Collectメソッド呼び出しは避けたほうがよい。単にガベージコレクタに実行タイミングを任せるのが最もよい。しかし、アプリケーションはCLRよりも自らの動作についての知識を持っているので、何らかの形のGCを明示的に強制実行することによって、状況を改善できる場合もある。たとえば、ユーザーがデータファイルを保存したあと、全世代のフルコレクションを強制するのは意味のないことではない。ページがアンロードされたときにフルコレクションを実行するインターネットブラウザも想像できる。何らかの非常に長い処理の実行中も、GCを強制するとよいかもしれない。こうすれば、GCがCPU時間を消費しているという事実を隠し、ユーザーがアプリケーションと対話しているときにGCが発生することを防ぐことができる。

GC型は、WaitForPendingFinalizersメソッドも提供している。このメソッドは、Fリーチャブルキューを処理しているスレッドが各オブジェクトのFinalizeメソッドを呼び出してキューを空にするまで、呼び出し元スレッドを一時停止させる。しかし、ほとんどのアプリケーションでは、このメソッドを呼び出す必要はないだろう。

最後に、ガベージコレクタは、オブジェクトが現在どのジェネレーションに所属しているかを返す2つのメソッドを提供している。

Int32 GetGeneration(Object obj)
Int32 GetGeneration(WeakReference wr)

GetGenerationの第1バージョンは引数としてオブジェクト参照、第2バージョンはWeakReference参照を取る。もちろん、返される値は、0以上GC.MaxGeneration以下である。

Figure 5 のコードは、ジェネレーションの仕組みを理解するうえで役に立つだろう。このコードは、今述べたGCメソッドの使い方も示している。


private static void GenerationDemo() {
    // Letユs see how many generations the GCH supports (we know it's 2)
    Display("Maximum GC generations: " + GC.MaxGeneration);

    // Create a new BaseObj in the heap
    GenObj obj = new GenObj("Generation");

    // Since this object is newly created, it should be in generation 0
    obj.DisplayGeneration();    // Displays 0

    // Performing a garbage collection promotes the object's generation
    Collect();
    obj.DisplayGeneration();    // Displays 1

    Collect();
    obj.DisplayGeneration();    // Displays 2

    Collect();
    obj.DisplayGeneration();    // Displays 2   (max generation)

    obj = null;         // Destroy the strong reference to this object

    Collect(0);         // Collect objects in generation 0
    WaitForPendingFinalizers();    // We should see nothing

    Collect(1);         // Collect objects in generation 1
    WaitForPendingFinalizers();    // We should see nothing

    Collect(2);         // Same as Collect()
    WaitForPendingFinalizers();    // Now, we should see the Finalize
                                   // method run

    Display(-1, "Demo stop: Understanding Generations.", 0);

 Figure 5 GC メソッドの使用例

マルチスレッド アプリケーションでの処理性能

今までの節では、GCのアルゴリズムと最適化について説明してきた。しかし、この議論は1つの大きな前提条件を持っていた。それは、スレッドが1つしか実行されていないというものである。現実の世界では、複数のスレッドが管理ヒープにアクセスしていたり、管理ヒープ内に確保されたオブジェクトを操作していたりする可能性が高い。あるスレッドがGCを発生させたら、ガベージコレクタはオブジェクトのメモリアドレスを変更してオブジェクトを移動してしまうかもしれないので、ほかのスレッドはオブジェクト (自らのスタックにあるオブジェクト参照も含まれる) にアクセスしてはならない。

そこで、ガベージコレクタがGCを実行しようとしたときには、管理下コードを実行するすべてのスレッドを一時停止させなければならない。CLRは、スレッドを安全に一時停止させ、GCを実行できるようにするためのメカニズムを複数持っている。複数のメカニズムが用意されているのは、できる限り長い間スレッドを実行し、できる限り多くのオーバーヘッドを削減するためである。本稿では細部を全面的に説明することは避けたい。MicrosoftがGCの実行のためのオーバーヘッドを削減するためにかなりの仕事をしているということを言っておけば十分だろう。Microsoftは、効率的なガベージコレクションを保証するために、これらのメカニズムを絶えず改良し続けるだろう。

では以下に、アプリケーションが複数のスレッドを持っているときに、ガベージコレクタが使うメカニズムの一部を説明する。

完全に割り込み可能なコード ガベージコレクタは、GC開始時にすべてのアプリケーションスレッドを一時停止させる。次にガベージコレクタはスレッドが一時停止された位置を調べ、JIT (just-in-time) コンパイラが生成した表を使って、スレッドが停止したメソッド内の位置、コードが現在アクセスしているオブジェクト参照、それらの参照が保持されている位置 (変数、CPUレジスタなど) を判定する。

ハイジャック ガベージコレクタは、リターンアドレスが、ある特別な関数を指すようにスレッドのスタックを書き換えることができる。現在実行中のメソッドが制御を返したら、この特別な関数が実行され、スレッドを一時停止させる。スレッドに実行経路をこのように盗むことは、スレッドのハイジャックと呼ばれる。GCが終了したら、スレッドは実行を再開し、本来の呼び出し元のメソッドに制御を返す。

セーフポイント JITコンパイラは、メソッドをコンパイルする過程で、GCが保留状態になっているかどうかをチェックする特別な関数に対する呼び出しを挿入できる。もしそうなら、スレッドを一時停止させてGCを実行し、終わったところでスレッドの実行を再開する。コンパイラがこのようなメソッド呼び出しを挿入する位置をGCセーフポイントと呼ぶ。

スレッドハイジャックメカニズムでは、非管理下コードを実行しているスレッドなら、GC実行中でも実行を継続できることに注意していただきたい。非管理下コードは、オブジェクトが“ピン留め”されていてオブジェクト参照を持たない場合を除き、管理ヒープのオブジェクトにはアクセスしないので、これが問題になることはない。“ピン留め”されたオブジェクトとは、ガベージコレクタがメモリ内で移動できないオブジェクトである。現在非管理下コードを実行中のスレッドが管理下コードに制御を返したら、スレッドはハイジャックされ、GCが完了するまで一時停止させられる。

以上のメカニズムのほかにも、ガベージコレクタは、アプリケーションが複数のスレッドを持っているときの、オブジェクトのメモリ確保とガベージコレクションの処理性能を向上させるためのいくつかの最適化機能を持っている。

同期不要のメモリ確保 マルチプロセッサシステムでは、管理ヒープのジェネレーション0は、スレッド当たり1つずつ、複数のメモリアリーナに分割される。こうすれば、複数のスレッドが同時にメモリ確保を行なっても、ヒープに対する排他的なアクセスは不要になる。

スケーラブルなコレクション 実行エンジンのサーババージョン (MSCorSvr.dll) を実行するマルチプロセッサシステムでは、管理ヒープは、CPUごとに1つずつの複数のセクションに分割される。GCが開始されると、ガベージコレクタはCPU当たり1つずつのスレッドを使う。つまり、すべてのスレッドがそれぞれのセクションに対して同時にGCを行なう。実行エンジンのワークステーションバージョン (MSCorWks.dll) は、この機能をサポートしていない。


大きなオブジェクトのガベージコレクション

処理性能の向上ということでは、注目すべき機能がもう1つある。大きなオブジェクト (20000バイト以上) は、専用の大オブジェクトヒープから確保される。このヒープのオブジェクトは、小オブジェクトと同様に、今まで述べてきたような形で完結化され解放される。しかし、20000バイトのメモリブロックをヒープ内でシフトダウンしてもCPU時間を浪費するだけなので、大規模オブジェクトはメモリコンパクションを受けない。

以上のメカニズムが、どれもアプリケーションコードに対しては透過的だということに注意していただきたい。プログラマからは、1つの管理ヒープがあるだけに見える。これらのメカニズムは、アプリケーションの処理性能を向上させるためにのみ存在する。

ガベージコレクションの監視

MicrosoftのCLRチームは、CLRの処理のリアルタイム統計を提供する一連のパフォーマンスカウンタを作成した。これらの統計情報は、Windows 2000のシステムモニタActiveXコントロールを使って表示できる。システムモニタコントロールにアクセスするための最も簡単な方法は、PerfMon.exeを起動し、ツールバーの「+」ボタンを選択して、[カウンタの追加]ダイアログボックス (Figure 6) をオープンするというものである。

Dd297765.Garbage_2_Fig06(ja-jp,MSDN.10).jpg
 Figure 6 パフォーマンス カウンタの追加

CLRのガベージコレクタを監視するためには、COM+ Memory Performanceオブジェクトを選択する。次に、インスタンスリストボックスから目的のアプリケーションを選択する。最後に、監視したいカウンタの集合を選択し、「閉じる」ボタンの上の「追加」ボタンを押す。すると、システムモニタは選択されたリアルタイム統計のグラフを描く。Figure 7 は個々のカウンタの機能をまとめたものである。

カウンタ 説 明
# Bytes in all Heaps ジェネレーション 0、1、2 および大規模オブジェクト ヒープの全バイト数。この値は、ガベージ コレクタが確保済みオブジェクトの格納のために使っているメモリのサイズを示す
# GC Handles 現在の GC ハンドルの総数
# Gen 0 Collections ジェネレーション 0 (最も新しい) オブジェクトのコレクションの数
# Gen 1 Collections ジェネレーション 1 オブジェクトのコレクションの数
# Gen 2 Collections ジェネレーション 2 (最も古い) オブジェクトのコレクションの数
# Included GC オブジェクトの確保中ではなく、明示的な呼び出しによって (たとえば、Classlib からの呼び出し) GC が実行された回数
# Pinned Objects 未実装
# of Sink Blocks in use 同期プリミティブは、シンク ブロックを使う。シンク ブロック データはオブジェクトに属し、オンデマンドで確保される
# Total Committed Bytes すべてのヒープのコミットされたバイトの総数
% Time in GC 最初のサンプル以降にガベージ コレクションの実行に使った時間を、最後のサンプルからの時間で除算した値
Allocated Bytes / sec ガベージ コレクタが 1 秒当たりに確保したバイト数の割合。この値が更新されるのは個々のメモリ確保のタイミングではなく、ガベージ コレクションのタイミングである。この値は割合なので、2 回の GC の間の時間は 0 であることもある
Finalization Survivors 完結かメソッドが参照を作成したために、ガベージ コレクションを生き残ったクラスの数
Gen 0 Heap Size ジェネレーション 0 (最も新しい) ヒープのバイト数
Gen 0 Promoted Bytes / sec ジェネレーション 0 (最も新しい) からジェネレーション 1 に昇格した、1 秒当たりのバイト数。メモリはガベージ コレクションを生き残った時に昇格する
Gen 1 Heap Size ジェネレーション 1 ヒープのバイト数
Gen 1 Promoted Bytes / sec ジェネレーション 1 からジェネレーション 2 (最も古い) に昇格した、1 秒当たりのバイト数。メモリはガベージ コレクションを生き残った時に昇格する。しかし、ジェネレーション 2 が上限なので、ジェネレーション 2 よりも先に昇格するオブジェクトはない
Gen 2 Heap Size ジェネレーション 2 (最も古い) ヒープのバイト数
Learge Object Heap Size 大規模オブジェクト ヒープのバイト数
Promoted Memory from Gen 0 ガベージ コレクションを生き残り、ジェネレーション 0 からジェネレーション 1 に昇格したメモリのバイト数
Promoted Memory from Gen 1 ガベージ コレクションを生き残り、ジェネレーション 1 からジェネレーション 2 に昇格したメモリのバイト数

 Figure 7 監視すべきカウンタ


まとめ

これでガベージコレクションについての話題は、ほぼ網羅することができた。Part I はリソースの確保方法、自動ガベージコレクションの仕組み、完結化機能を使ったオブジェクトのクリーンアップ、復活によるオブジェクトへのアクセスの回復、その背景について説明した。Part II は、オブジェクトに対する強弱2種類の参照の実装の様子、オブジェクトをジェネレーションに分類して処理性能を向上させる仕組み、Ssytem.GC型を使ってガベージコレクションを直接操作する方法を説明した。また、ガベージコレクタがマルチスレッドアプリケーションの処理性能を向上させるためのメカニズム、20000バイト以上のオブジェクトの処理、Windows 2000システムモニタを使ったガベージコレクションの処理性能の監視方法についても説明した。以上の知識があれば、アプリケーションのメモリ管理を単純化し、処理性能を向上させることができるはずである。


表示: