web-dev-qa-db-ja.com

C#でのガベージコレクターの監視

多くのパフォーマンス問題が発生しているWPFアプリケーションがあります。最悪の場合、アプリケーションが数秒間フリーズしてから再度実行されることがあります。

私は現在、このフリーズが何に関連している可能性があるかを確認するためにアプリケーションをデバッグしており、それを引き起こしている可能性のあるものの1つがガベージコレクタであると考えています。私のアプリケーションは非常に限られた環境で実行されているので、ガベージコレクターは実行時にマシンのすべてのリソースを使用でき、アプリケーションに何も残していないと思います。

この仮説を確認するために、次の記事を見つけました: ガベージコレクション通知 および 。NET 4.0のガベージコレクション通知 。これらは、ガベージコレクターが実行を開始したときにアプリケーションに通知する方法を説明しています。そしてそれが終わったとき。

したがって、これらの記事に基づいて、通知を取得するために以下のクラスを作成しました。

public sealed class GCMonitor
{
    private static volatile GCMonitor instance;
    private static object syncRoot = new object();

    private Thread gcMonitorThread;
    private ThreadStart gcMonitorThreadStart;

    private bool isRunning;

    public static GCMonitor GetInstance()
    {
        if (instance == null)
        {
            lock (syncRoot)
            {
                instance = new GCMonitor();
            }
        }

        return instance;
    }

    private GCMonitor()
    {
        isRunning = false;
        gcMonitorThreadStart = new ThreadStart(DoGCMonitoring);
        gcMonitorThread = new Thread(gcMonitorThreadStart);
    }

    public void StartGCMonitoring()
    {
        if (!isRunning)
        {
            gcMonitorThread.Start();
            isRunning = true;
            AllocationTest();
        }
    }

    private void DoGCMonitoring()
    {
        long beforeGC = 0;
        long afterGC = 0;

        try
        {

            while (true)
            {
                // Check for a notification of an approaching collection.
                GCNotificationStatus s = GC.WaitForFullGCApproach(10000);
                if (s == GCNotificationStatus.Succeeded)
                {
                    //Call event
                    beforeGC = GC.GetTotalMemory(false);
                    LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "GC is about to begin. Memory before GC: %d", beforeGC);
                    GC.Collect();

                }
                else if (s == GCNotificationStatus.Canceled)
                {
                    LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was cancelled");
                }
                else if (s == GCNotificationStatus.Timeout)
                {
                    LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was timeout");
                }
                else if (s == GCNotificationStatus.NotApplicable)
                {
                    LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was not applicable");
                }
                else if (s == GCNotificationStatus.Failed)
                {
                    LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event failed");
                }

                // Check for a notification of a completed collection.
                s = GC.WaitForFullGCComplete(10000);
                if (s == GCNotificationStatus.Succeeded)
                {
                    //Call event
                    afterGC = GC.GetTotalMemory(false);
                    LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "GC has ended. Memory after GC: %d", afterGC);

                    long diff = beforeGC - afterGC;

                    if (diff > 0)
                    {
                        LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "Collected memory: %d", diff);
                    }

                }
                else if (s == GCNotificationStatus.Canceled)
                {
                    LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was cancelled");
                }
                else if (s == GCNotificationStatus.Timeout)
                {
                    LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was timeout");
                }
                else if (s == GCNotificationStatus.NotApplicable)
                {
                    LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was not applicable");
                }
                else if (s == GCNotificationStatus.Failed)
                {
                    LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event failed");
                }

                Thread.Sleep(1500);
            }
        }
        catch (Exception e)
        {
            LogHelper.Log.Error("  ********************   Garbage Collector Error  ************************ ");
            LogHelper.LogAllErrorExceptions(e);
            LogHelper.Log.Error("  -------------------   Garbage Collector Error  --------------------- ");
        }
    }

    private void AllocationTest()
    {
        // Start a thread using WaitForFullGCProc.
        Thread stress = new Thread(() =>
        {
            while (true)
            {
                List<char[]> lst = new List<char[]>();

                try
                {
                    for (int i = 0; i <= 30; i++)
                    {
                        char[] bbb = new char[900000]; // creates a block of 1000 characters
                        lst.Add(bbb);                // Adding to list ensures that the object doesnt gets out of scope
                    }

                    Thread.Sleep(1000);
                }
                catch (Exception ex)
                {
                    LogHelper.Log.Error("  ********************   Garbage Collector Error  ************************ ");
                    LogHelper.LogAllErrorExceptions(e);
                    LogHelper.Log.Error("  -------------------   Garbage Collector Error  --------------------- ");
                }
            }


        });
        stress.Start();
    }
}

そして、以下のapp.configファイルにgcConcurrentオプションを追加しました。

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net-net-2.0"/>
  </configSections>

  <runtime>
    <gcConcurrent enabled="false" />
  </runtime>

  <log4net>
    <appender name="Root.ALL" type="log4net.Appender.RollingFileAppender">
      <param name="File" value="../Logs/Root.All.log"/>
      <param name="AppendToFile" value="true"/>
      <param name="MaxSizeRollBackups" value="10"/>
      <param name="MaximumFileSize" value="8388608"/>
      <param name="RollingStyle" value="Size"/>
      <param name="StaticLogFileName" value="true"/>
      <layout type="log4net.Layout.PatternLayout">
      <param name="ConversionPattern" value="%date [%thread] %-5level - %message%newline"/>
      </layout>
    </appender>
    <root>
      <level value="ALL"/>
      <appender-ref ref="Root.ALL"/>
    </root>
  </log4net>

  <appSettings>
    <add key="setting1" value="1"/>
    <add key="setting2" value="2"/>
  </appSettings>
  <startup>
    <supportedRuntime version="v2.0.50727"/>
  </startup>

</configuration>

ただし、アプリケーションが実行されると、ガベージコレクターが実行されるという通知が送信されないように見えます。 DoGCMonitoringにブレークポイントを設定したところ、(s == GCNotificationStatus.Succeeded)および(s == GCNotificationStatus.Succeeded)の条件が満たされないため、これらのifsステートメントの内容が実行されないようです。

何が悪いのですか?

注:WPFと.NET Framework 3.5でC#を使用しています。

更新1

AllocationTestメソッドでGCMonitorテストを更新しました。この方法は、テストのみを目的としています。ガベージコレクターを強制的に実行するのに十分なメモリが割り当てられていることを確認したかっただけです。

更新2

DoGCMonitoringメソッドが更新され、WaitForFullGCApproachメソッドとWaitForFullGCCompleteメソッドの戻り値に対する新しいチェックが追加されました。これまで見てきたことから、アプリケーションは(s == GCNotificationStatus.NotApplicable)状態に直接移行しています。だから私は私が望む結果を得るのを止めているどこかに間違った設定があると思います。

GCNotificationStatus列挙型のドキュメントは here にあります。

33
Felipe

コードのどこにも GC.RegisterForFullGCNotification(int,int) はありません。 WaitForFullGC[xxx]メソッドを使用しているようですが、通知を登録していません。これが、NotApplicableステータスを取得する理由です。

ただし、GCが問題であるとは思えませんが、可能な限り、すべてのGCモードと、何が起こっているのかを判断するための最良の方法について知っておくとよいでしょう。 .NETのガベージコレクションには、サーバーとワークステーションの2つのモードがあります。どちらも同じ未使用のメモリを収集しますが、その方法は少しずつ異なります。

  • サーバーバージョン-このモードは、サーバー側アプリケーションを使用していることをGCに伝え、これらのシナリオに合わせてコレクションを最適化しようとします。 CPUごとに1つずつ、ヒープをいくつかのセクションに分割します。 GCが開始されると、各CPUで1つのスレッドが並列に実行されます。これをうまく機能させるには、複数のCPUが本当に必要です。サーバーのバージョンはGCに複数のスレッドを使用しますが、下記の同時ワークステーションGCモードとは異なります。各スレッドは、非並行バージョンのように機能します。

  • Workstation Version-このモードは、クライアント側アプリケーションを使用していることをGCに通知します。サーバーバージョンよりもリソースが制限されているため、GCスレッドは1つしかありません。ただし、Workstationバージョンには、同時と非同時の2つの構成があります。

    • Concurrent-これは、ワークステーションGCが使用されるときにデフォルトでオンになるバージョンです(これは、WPFアプリケーションの場合です)。 GCは常に、アプリケーションの実行時にオブジェクトを収集対象としてマークしている別のスレッドで実行されます。さらに、特定の世代でメモリを圧縮するかどうかを選択し、パフォーマンスに基づいてその選択を行います。圧縮が行われると、コレクションを実行するためにすべてのスレッドをフリーズする必要がありますが、このモードを使用すると、応答しないアプリケーションが表示されることはほとんどありません。これにより、使用に適したインタラクティブなエクスペリエンスが作成され、コンソールまたはGUIアプリに最適です。
    • Non-Concurrent-これは、必要に応じて、使用するアプリケーションを構成できるバージョンです。このモードでは、GCスレッドはGCが開始されるまでスリープ状態になり、その後、ガベージであるすべてのオブジェクトツリーにマークを付け、メモリを解放し、および圧縮します。スレッドは中断されます。これにより、アプリケーションがshortの間応答しなくなることがあります。

バックグラウンドで行われるため、並行コレクターで通知を登録することはできません。アプリケーションがコンカレントコレクターを使用していない可能性があります(app.configgcConcurrentが無効になっていることに気づきましたが、これはテスト専用のようです)。その場合、大量のコレクションがあると、アプリケーションがフリーズするのが確実にわかります。これが、コンカレントコレクタを作成した理由です。 GCモードのタイプは、コードで部分的に設定でき、アプリケーション構成とマシン構成で完全に設定できます。

アプリケーションが使用しているものを正確に把握するために何ができますか?実行時に、静的GCSettingsクラスをクエリできます(System.Runtime内)。 GCSettings.IsServerGCは、サーバーバージョンでワークステーションを実行しているかどうかを通知し、 GCSettings.LatencyMode は、並行、非並行、または特別なものを使用しているかどうかを通知しますここでは実際には適用できないコードを設定する必要があります。私はそれが良い出発点になると思います、そしてそれがあなたのマシンでうまく動いているが、生産ではない理由を説明することができます。

構成ファイルでは、<gcConcurrent enabled="true|false"/>または<gcServer enabled="true|false"/>がガベージコレクターのモードを制御します。これはapp_configファイル(実行中のアセンブリの横にあります)または%windir%\Microsoft.NET\Framework\[version]\CONFIG\にあるmachine.configファイルのにあることに注意してください

また、リモートでWindowsパフォーマンスモニターを使用して、.NETガベージコレクションの本番マシンのパフォーマンスカウンターにアクセスし、それらの統計を表示することもできます。イベントトレースfor Windows(ETW)を使用すると、すべてリモートで同じことができます。パフォーマンスモニターの場合、.NET CLR Memoryオブジェクトが必要であり、インスタンスリストボックスでアプリケーションを選択します。

40