LoginSignup
19
15

More than 1 year has passed since last update.

ガベージ・コレクタの挙動を実験してみた

Last updated at Posted at 2017-11-18

.Net Framework 4.5.2のガベージ・コレクタの挙動を調べるために、C#のコードで実験してみた記事です。

※ 本記事のサンプルコードは、川俣晶氏の以下記事を参考にさせていただきました。

ローカル変数の場合

以下コードでは、Queue<string[]>型のオブジェクトをローカル変数として宣言します。キューに巨大データを詰め込んで拡張した後、GC.Collect()でマネージドヒープのゴミ回収をしますが、GC.Collect()前後でのメモリ使用量(近似値)を出力しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GCTest
{
    class Program
    {
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            var q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
        static Queue<string[]> CreateQueue()
        {
            var q = new Queue<string[]>();

            string[] a = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                a[i] = new String('A', 1000);
            }
            q.Enqueue(a);

            string[] b = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                b[i] = new String('A', 1000);
            }
            q.Enqueue(b);

            string[] c = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                c[i] = new String('A', 1000);
            }
            q.Enqueue(c);

            string[] d = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                d[i] = new String('A', 1000);
            }
            q.Enqueue(d);

            return q;
        }
    }
}

リリースモードで実行すると、結果は以下となりました。

実行結果(リリースモード)
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :40124

GC.Collect()によって7.7MBのメモリが解放されたのは分かるのですが、GC.Collect()の呼出し時点ではqはスコープを抜けてないのだから、メモリ解放されるのおかしくね?と思いました。

ですが、これってGC.Collect()からqのスコープ末尾「}」までの間に、qが使用されていないから解放されたのかなあと思います。

試しに、qを使用するコードを追加してみました。

qを使用してガベコレの対象から除外
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            var q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            q.Count(); // qを参照
            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }

qの使用を追加した上記コードをリリースモードで実行してみると、ライフタイムが延長されたのか、メモリ解放されませんでした。

実行結果(リリースモード)
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :8120136

ちなみにデバッグモードで実行すると、最適化されないせいか、qを使用しなくてもライフタイムが延長せず、メモリも解放されていませんね。

実行結果(デバッグモード)
The highest generation is 2.
1. Before CreateQueue() :31772
2. After CreateQueue()  :8113124
3. After GC.Collect()   :8111892

GC.KeepAlive()

GC.KeepAlive()は、オブジェクトのライフタイムを延長します。このメソッドは「この時点までライフタイムを延長します」とマーカするものです。

GC.Keepalive()でライフタイムを延長
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            var q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            GC.KeepAlive(q); // この時点までqのライフタイムを延長

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }

ライフタイムが延長され、予想通りガベコレの回収対象になりませんでした。

実行結果(GC.KeepAlive()を使用)
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue()  :8219792
3. After GC.Collect()   :8224688

オブジェクトのライフタイムを制限

ブロックで閉じてオブジェクトのライフタイムをexpiredさせれば、デバッグモードでもガベコレに回収されんじゃね?と思い、以下のコードを書いてみました。オブジェクトq{}内に閉じ込めています。

スコープで閉じる(デバッグモード)
        static void Main(string[] args)
        {
            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
            {
                var q = CreateQueue();
            }
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");

            GC.Collect();

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }

あれ、解放されませんね…。デバッグモードってそういうものなんですね。

実行結果(デバッグモード)
The maximum generation is 2.
1. Before CreateQueue() :32088
2. After CreateQueue()  :8123664
3. After GC.Collect()   :8112208

ただし、GC.Collect()の前にqnullを代入すると、デバッグモードでもqが使用するリソースは解放されました。

nullを代入
        static void Main(string[] args)
        {
            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");
            {
                var q = CreateQueue();
                Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
                q = null;
            }
            GC.Collect();

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
実行結果(デバッグモード)
The maximum generation is 2.
1. Before CreateQueue() :32088
2. After CreateQueue()  :8123664
3. After GC.Collect()   :32100

static変数の場合

qをstatic変数にしてみました。

static変数にしてガベコレ対象から除外
        static Queue<string[]> q;
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }

するとどうでしょう。qはガベコレに回収されませんでした。まあ、staticなのだからそりゃそうですね。

実行結果(リリースモード)
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :8120136

static変数の場合(nullを代入)

qがガベコレの対象になるように、ガベコレ直前にq = null;を追加してみました。

q=nullにしてガベコレ対象にする
        static Queue<string[]> q;
        static void Main(string[] args)
        {
            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            q = CreateQueue();
            q = null;  // ガベコレの対象になるはず
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }

おお、ガベコレに回収されてる。やったね。

実行結果(リリースモード)
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :40124

static変数の場合(Dequeue()でキューを空にする)

次はnullを代入する代わりに、キューの全要素をDequeue()で空にしてみます。

Dequeue()でキューを空にしてガベコレ
        static Queue<string[]> q;
        static void Main(string[] args)
        {
            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");

            var slotCount = q.Count;
            for (int i = 0; i < slotCount; ++i)
                q.Dequeue();

            Console.WriteLine($"3. After Dequeue()  :{GC.GetTotalMemory(false)}");
            GC.Collect();
            Console.WriteLine($"4. After GC.Collect()   :{GC.GetTotalMemory(false)}");
            q = null;
            GC.Collect();
            Console.WriteLine($"5. After set null to q  :{GC.GetTotalMemory(false)}");
        }

解放されますね。要素数が大きいときは、Queue.TrimExcess()で空きスロットをshrinkすれば、よりオーバーヘッドが減ると思います。

実行結果(リリースモード)
The maximum generation is 2.
1. Before CreateQueue() :64584
2. After CreateQueue()  :8219792
3. After Dequeue()  :8219792
4. After GC.Collect()   :65048
5. After set null to q  :64728

static変数の場合(Clear()でキューを空にする)

Clear()でキューを空にする
        static Queue<string[]> q;
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");

            q.Clear();

            GC.Collect();
            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }

Clear()でも同じく、ガベコレに回収されるですね。

実行結果(リリースモード)
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :40184

static変数の場合(new演算子で領域を再割り当て)

q.Clear();の代わりに、q = new Queue<string[]>();に変更したところ、やはりガベコレに回収されました。

実行結果(リリースモード)
The maximum generation is 2.
1. Before CreateQueue() :39984
2. After CreateQueue()  :8113144
3. After GC.Collect()   :40156

newによる再割当て前の領域はダングリング参照とはならずに、ちゃんとガベコレに回収されるんですね(それがガベコレの仕事なのだが…)。

static変数の場合(クラスのstaicメンバ)

キュー(static領域)をメンバにもつクラスのインスタンス変数にnullを代入したケースです。

クラスのstaticメンバ(インスタンスは解放)
class QueueTest
{
    static public Queue<string[]> q;
}

namespace GCTest
{
    class Program
    {
        static void Main(string[] args)
        {

            Console.WriteLine($"The maximum generation is {GC.MaxGeneration}.");
            GC.Collect();

            Console.WriteLine($"1. Before CreateQueue() :{GC.GetTotalMemory(false)}");

            var aQueueTest = new QueueTest();
            QueueTest.q = CreateQueue();
            Console.WriteLine($"2. After CreateQueue()  :{GC.GetTotalMemory(false)}");

            aQueueTest = null;
            GC.Collect();

            Console.WriteLine($"3. After GC.Collect()   :{GC.GetTotalMemory(false)}");
        }
    // ...

インスタンスにnullを代入しても、メンバが参照するリソースはstatic領域にあるため、リソースはガベコレに回収されませんね。

実行結果(リリースモード)(キューをメンバにもつクラスのインスタンスにnullを代入)
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue()  :8219792
3. After GC.Collect()   :8224688

staticメンバにnullを代入してstatic領域のリソースへの参照を断つと、やっとガベコレに回収されました。

staticメンバにnullを代入
            // aQueueTest = null;
            QueueTest.q = null;
            GC.Collect();
実行結果(リリースモード)(staticメンバにnullを代入)
The maximum generation is 2.
1. Before CreateQueue() :64416
2. After CreateQueue()  :8219792
3. After GC.Collect()   :64752

おわりに

くぅ疲です。「ガベージ・コレクタ」というと、高度なアルゴリズムで武装された、頭の良い人だけが意識する近代兵器というイメージがあったのですが、こうやって実コードで動作を確認してみると、意外と身近なものに思えてくるものですね。筆者はガベコレのことが、ちょっと好きになりました。

19
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
15