.NET TIPS

ガベージ・コレクタを明示的に動作させるには?

株式会社ピーデー 川俣 晶
2003/05/02

 C++やVisual Basic 6.0の世界でプログラミングしてきた技術者が.NET Frameworkの世界に入ってきてまずおどろくのは、プログラムを実行していると、プロセスが使用するメモリ量がどんどん増えていくことである。「メモリ・リークか!?」と焦ることもあるが、これは正常な動作である。

 メモリの解放忘れは典型的なバグの要因であり、メモリ解放を自動化することによって、プログラムの信頼性は向上し、プログラマーの負担も減る。自動的なメモリ解放を行う機構は、ガベージ・コレクタと呼ばれ、解放する行為をガベージ・コレクションと呼ぶ。問題は、ガベージ・コレクションがいつ行われるかであるが、これはメモリが不足してきた場合や、明示的に動作を指示された場合にのみ行われる。つまり、メモリが潤沢に余っている場合には、プロセスの使用するメモリ量が増加するのが正常な動作である。そのままメモリ不足でプログラムが停止するのではないかと懸念する必要はない。メモリが不足してくれば、自動的に回収が行われる。

 そのため、プロセスが使用するメモリ量が増加しているとしても、通常はそれに対してプログラマー自身が何かの対処をする必要はない。むしろ、メモリ管理に下手に手を出そうとすると、かえって効率を落とす可能性がある。

それでもガベージ・コレクションを行うには

 以上のような説明を踏まえた上で、プログラム中で任意の時点でガベージ・コレクションを行いたい場合がある。例えば、ある処理を最速で実行したい場合など、処理中にガベージ・コレクションが発生しないよう、処理の開始前にガベージ・コレクションを行っておきたいこともあるだろう。それを行うにはGCクラス(System名前空間)のCollectメソッドを呼び出せばよい。実際に、このメソッドによって使用メモリ量が減少しているところを次のサンプル・プログラムで確認してみよう。

// gctest1.cs

using System;

class GCTest1 {
  static void Main() {

    Console.WriteLine(GC.GetTotalMemory(false)); // 18408

    string [] a = new string[1000];
    for (int i = 0; i < 1000; i++ ) {
      a[i] = new String('A',1000);
    }
    Console.WriteLine(GC.GetTotalMemory(false)); // 2045928

    a = null;
    Console.WriteLine(GC.GetTotalMemory(false)); // 2045928

    GC.Collect();
    Console.WriteLine(GC.GetTotalMemory(false)); // 19452
  }
}

// コンパイル方法:csc gctest1.cs
GC.Collectメソッドの動作を確認するためのサンプル・プログラム(C#版:gctest1.cs)
(各Console.WriteLineメソッド呼び出しの最後に付いているコメントは、その出力例を示している)
 
' gctest1.vb

Imports System

Module GCTest1
  Sub Main()
    Console.WriteLine(System.GC.GetTotalMemory(False))

    Dim a() As String = New String(1000) {}
    Dim i As Integer
    For i = 0 To 999
      a(i) = New String("A"c, 1000)
    Next
    Console.WriteLine(System.GC.GetTotalMemory(False))

    a = Nothing
    Console.WriteLine(System.GC.GetTotalMemory(False))

    System.GC.Collect()
    Console.WriteLine(System.GC.GetTotalMemory(False))
  End Sub
End Module

' コンパイル方法:vbc gctest1.vb
GC.Collectメソッドの動作を確認するためのサンプル・プログラム(VB.NET版:gctest1.vb)

 GC.GetTotalMemory(false)は、その時点で使用されているメモリ量を返すメソッドである。これにより、メモリ使用量を表示させて変化を見ている。ただし、クラス・ライブラリのリファレンスでは、GetTotalMemoryメソッドの説明において「現在割り当てられていると思われるバイト数を取得します」と表記されており、必ずしも正確な値ではない可能性があることを示唆している。

 さて、このサンプル・プログラムでは、1000文字の文字列を1000個含む配列を作成し、それを解放する処理を記述している。「a = null」(VB.NETでは「a = Nothing」)を実行した時点で、確保したメモリはすべて不要になったことを明示的に示すことができる。この時点で、確保したメモリは解放されてもよいのだが、実行結果例の表示を見ると、その段階ではメモリ使用量が減っていないことが分かるだろう。しかし、Collectメソッド実行後の数値を見ると明らかに減っている。つまり、ガベージ・コレクションが実行され、不要なメモリが回収されたのである。

 なお、最後の数値と最初の数値が異なっているのは、ここで問題にしたデータだけが確保されたメモリではないこと、それから、次項に述べるような賢い世代管理が行われていることなどが理由として考えられる。しかし、システム内部の構造に依存する話なので、このような数値差はあり得るものと考えて扱った方がよいだろう。

より高度なガベージ・コレクションを行う

 .NET Frameworkのガベージ・コレクタは、ジェネレーション(世代)という概念を持っており、ガベージ・コレクションを効率よく実行できる。ジェネレーションとは、生成されてから間もないオブジェクトと、時間が経過したオブジェクトを分ける概念である。一般的に、作成されてから時間が経過したオブジェクトは解放される可能性が低く、生成されてから間もないオブジェクトは解放される可能性が高いことから、後者のオブジェクトだけを調べて解放処理を行うことで効率アップするというものだ(作成から長時間が経過しても解放されないオブジェクトは、今後も長期にわたり使用され続ける可能性が高い)。

 このような構造であることから、ガベージ・コレクションを行う際に、ジェネレーションを意識した指定を行う価値がある。つまり、生成されてから間もないオブジェクトのみを対象として、回収漏れが起きる可能性はあるものの、素早くガベージ・コレクションを終了する場合と、もっと古いオブジェクトも対象として、時間はかかるがあらゆる使用済みオブジェクトを回収する場合である。Collectメソッドでは、パラメータによりジェネレーションを明示的に指定することができる。

 以下は、ジェネレーションを指定したガベージ・コレクションを行うサンプル・プログラムである。

// gctest2.cs

using System;

class GCTest2 {
  static void Main() {

    Console.WriteLine(GC.GetTotalMemory(false)); // 10216

    string [] a = new string[1000];
    for (int i = 0; i < 1000; i++) {
      a[i] = new String('A',1000);
    }
    GC.Collect();
    Console.WriteLine(GC.GetTotalMemory(false)); // 2043492

    string [] b = new string[1000];
    for (int i = 0; i < 1000; i++) {
      b[i] = new String('A',1000);
    }
    GC.Collect();
    Console.WriteLine(GC.GetTotalMemory(false)); // 4067520

    string [] c = new string[1000];
    for (int i = 0; i < 1000; i++) {
      c[i] = new String('A',1000);
    }
    GC.Collect();
    Console.WriteLine(GC.GetTotalMemory(false)); // 6091548

    string [] d = new string[1000];
    for (int i = 0; i < 1000; i++) {
      d[i] = new String('A',1000);
    }
    Console.WriteLine(GC.GetTotalMemory(false)); // 8123164

    a = null;
    b = null;
    c = null;
    d = null;

    GC.Collect(0);
    Console.WriteLine(GC.GetTotalMemory(false)); // 6091676

    GC.Collect(1);
    Console.WriteLine(GC.GetTotalMemory(false)); // 4067692

    GC.Collect(2);
    Console.WriteLine(GC.GetTotalMemory(false)); // 19580
  }
}

// コンパイル方法:csc /debug+ gctest2.cs
ジェネレーションを指定したガベージ・コレクションのサンプル・プログラム(C#版:gctest2.cs)
(各Console.WriteLineメソッド呼び出しの最後に付いているコメントは、その出力例を示している)
 
' gctest2.vb

Imports System

Module GCTest2
  Sub Main()
    Console.WriteLine(System.GC.GetTotalMemory(False))
    Dim i As Integer

    Dim a() As String = New String(1000) {}
    For i = 0 To 999
      a(i) = New String("A"c, 1000)
    Next
    System.GC.Collect()
    Console.WriteLine(System.GC.GetTotalMemory(False))

    Dim b() As String = New String(1000) {}
    For i = 0 To 999
      b(i) = New String("A"c, 1000)
    Next
    System.GC.Collect()
    Console.WriteLine(System.GC.GetTotalMemory(False))

    Dim c() As String = New String(1000) {}
    For i = 0 To 999
      c(i) = New String("A"c, 1000)
    Next
    System.GC.Collect()
    Console.WriteLine(System.GC.GetTotalMemory(False))

    Dim d() As String = New String(1000) {}
    For i = 0 To 999
      d(i) = New String("A"c, 1000)
    Next
    Console.WriteLine(System.GC.GetTotalMemory(False))

    a = Nothing
    b = Nothing
    c = Nothing
    d = Nothing

    System.GC.Collect(0)
    Console.WriteLine(System.GC.GetTotalMemory(False))

    System.GC.Collect(1)
    Console.WriteLine(System.GC.GetTotalMemory(False))

    System.GC.Collect(2)
    Console.WriteLine(System.GC.GetTotalMemory(False))
  End Sub
End Module

' コンパイル方法:vbc /debug+ gctest2.vb
ジェネレーションを指定したガベージ・コレクションのサンプル・プログラム(VB.NET版:gctest2.vb)

 なお、このサンプル・プログラムは、コンパイラの最適化によって意図しない数値になるので、最適化を抑止するためにコンパイル・オプションで「/debug+」を指定して、デバッグ・バージョンとしてビルドする必要がある。

 ここでの本題は、ジェネレーションを指定したガベージ・コレクションである。これには、Collectメソッドのパラメータに、どのジェネレーションまでを対象にするか、数値を指定することで行う。

 さて、このサンプル・プログラムを理解するには、2つの知識が必要である。まず、現在の.NET Frameworkには3つのジェネレーション(ジェネレーション0、ジェネレーション1、ジェネレーション2)があるということ。そして、Collectメソッドでガベージ・コレクションを行うと、その時点で残ったオブジェクトはジェネレーションが1つ古い方にずれるということである。

 例えば上記のサンプル・プログラムでは、配列aが参照する配列オブジェクトは、生成された時点ではジェネレーション0に属している。そのため、もしすぐに参照を解放してしまえば、次のCollectメソッドで回収されてしまうことになる。しかし、この例では参照を解放することなくCollectメソッドを実行しているので、解放される代わりにジェネレーションが1つ上がってジェネレーション1になる。ジェネレーションが上がるということは、不要になっても回収されなくなる可能性が増えるということを意味する。通常、ガベージ・コレクタはジェネレーション0を対象にガベージ・コレクションを行い、それで十分な量のメモリを解放できれば、ジェネレーション1は調べないからである。

 さて、ジェネレーションは2までしかないので、配列aが参照する配列オブジェクトは3回のCollectメソッド実行後にはジェネレーション2となっている。a = null(VB.NETではa = Nothing)を実行することで、この配列の参照は消滅し、回収の対象になる。しかし、次のGC.Collect(0)というメソッド呼び出しでは、ジェネレーション0までを対象にガベージ・コレクションを行うため、ジェネレーション2のこの配列が処理されることはない。GC.Collect(2)を実行したときに初めて処理の対象になり回収される。この一連の動きは、ほかの配列オブジェクトでも同様である。プログラムの処理される順番と、実行結果を見比べながら、どこでどの配列オブジェクトがジェネレーションいくつになっていて、どれが回収されるかを順番に追いかけてみると理解が深まるだろう。

 このサンプル・プログラムと実行結果を見ると、ジェネレーションを意識したメモリの回収処理を行うと、意図的に回収するタイミングをコントロールできるかのように見えるかもしれない。しかし、ガベージ・コレクタはシステムが必要とした場合には随時実行されるので、完全に意図的にコントロールすることはできない。基本的には、ガベージ・コレクションは、意識的に操作するものではなく、システムに任せるものといえる。

 なお、85,000byte以上のオブジェクトは常にジェネレーション2に置かれ、ジェネレーション増加の対象にならないとされている。これは、サイズの大きなオブジェクトの管理はコストが大きく、頻繁に回収してしまうとパフォーマンスに悪影響を与えるためだ。End of Article

参考文献
プログラミング Microsoft .NET Framework
著者 Jeffery Richter
訳者 吉松 史彰
出版社 日経BPソフトプレス
第19章 自動メモリ管理(ガベージコレクション)
 
カテゴリ:クラス・ライブラリ 処理対象:メモリ管理
使用ライブラリ:GCクラス(System名前空間)
 
この記事と関連性の高い別の.NET TIPS
確実な終了処理を行うには?
確保したリソースを忘れずに解放するには?[C#/VB]
このリストは、(株)デジタルアドバンテージが開発した
自動関連記事探索システム Jigsaw(ジグソー) により自動抽出したものです。
generated by

「.NET TIPS」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間