Подтвердить что ты не робот

Мониторинг сборщика мусора в С#

У меня есть приложение WPF, которое испытывает много проблем с производительностью. Хуже всего то, что иногда приложение просто замерзает в течение нескольких секунд перед повторным запуском.

В настоящее время я отлаживаю приложение, чтобы узнать, к чему относится это замораживание, и я считаю, что одним из факторов, которые могут быть причиной этого, является сборщик мусора. Поскольку мое приложение работает в очень ограниченной среде, я считаю, что сборщик мусора может использовать все ресурсы компьютера, когда он запущен, и не оставляет его в нашем приложении.

Чтобы проверить эту гипотезу, я нашел следующие статьи: Уведомления о сборе мусора и Уведомления о сборе мусора в .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();
    }
}

И я добавил параметр gcConcurrent в файл app.config(ниже):

<?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.

ОБНОВЛЕНИЕ 1

Обновлен мой тест GCMonitor с помощью метода AllocationTest. Этот метод предназначен только для тестирования. Я просто хотел удостовериться, что выделено достаточно памяти для принудительного запуска сборщика мусора.

ОБНОВЛЕНИЕ 2

Обновлен метод DoGCMonitoring с новыми проверками на возврат методов WaitForFullGCApproach и WaitForFullGCCcomplete. Из того, что я видел до сих пор, мое приложение переходит непосредственно к условию (s == GCNotificationStatus.NotApplicable). Поэтому я думаю, что у меня некоторая некорректная конфигурация, которая мешает мне получить желаемые результаты.

Документацию для перечисления GCNotificationStatus можно найти здесь.

4b9b3361

Ответ 1

Я не вижу GC.RegisterForFullGCNotification(int,int) в любом месте вашего кода. Похоже, вы используете методы WaitForFullGC[xxx], но никогда не регистрируетесь для уведомления. Вероятно, почему вы получаете статус NotApplicable.

Однако я сомневаюсь, что GC - ваша проблема, хотя это возможно, я полагаю, было бы хорошо знать обо всех режимах GC, которые есть, и о лучших способах определения того, что происходит. В .NET есть два режима Garbage Collection: сервер и рабочая станция. Они оба собирают одну и ту же неиспользуемую память, однако, как это было сделано, все так немного отличается.

  • Версия сервера. Этот режим сообщает GC, что вы используете приложение на стороне сервера и пытается оптимизировать коллекции для этих сценариев. Он разделяет кучу на несколько секций, по 1 на процессор. Когда GC запускается, он будет запускать один поток на каждом CPU параллельно. Вы действительно хотите, чтобы несколько процессоров работали хорошо. Хотя версия сервера использует несколько потоков для GC, это не то же самое, что и режим параллельной рабочей станции GC, указанный ниже. Каждый поток действует как неконкурентная версия.

  • Версия рабочей станции. Этот режим сообщает GC, что вы используете приложение на стороне клиента. Он показывает, что у вас более ограниченные ресурсы, чем версия сервера, и поэтому существует только один поток GC. Однако есть две конфигурации версии рабочей станции: параллельная и неконкурентная.

    • Параллельный - эта версия включена по умолчанию всякий раз, когда используется GC рабочей станции (это будет иметь место для вашего приложения WPF). GC всегда работает в отдельном потоке, который всегда маркирует объекты для сбора, когда приложение запущено. Кроме того, он выбирает, следует ли уплотнять память в определенных поколениях, и делает выбор на основе производительности. По-прежнему необходимо заморозить все потоки для запуска коллекции, если выполняется сжатие, но при использовании этого режима вы почти никогда не увидите невосприимчивого приложения. Это создает лучший интерактивный опыт для использования и лучше всего подходит для консольных или графических приложений.
    • Non-Concurrent. Это версия, в которой вы можете настроить приложение, если хотите. В этом режиме поток GC засыпает до тех пор, пока не будет запущен GC, а затем он помечает все деревья объектов, которые являются мусором, освобождает память и уплотняет ее, пока все остальные потоки приостановлены. Это может привести к тому, что приложение иногда перестает отвечать на запросы в течение короткого периода времени.

Вы не можете регистрироваться для уведомлений в параллельном коллекторе, поскольку это сделано в фоновом режиме. Возможно, что ваше приложение не использует параллельный сборщик (я заметил, что вы отключили gcConcurrent в app.config, но, похоже, это только для тестирования?). Если это произойдет, вы можете, конечно, увидеть, что ваше приложение замерзает, если есть большие коллекции. Вот почему они создали параллельный сборщик. Тип режима GC может быть частично установлен в коде и полностью установлен в конфигурациях приложений и конфигурациях машин.

Что мы можем сделать, чтобы точно определить, что использует наше приложение? Во время выполнения вы можете запросить статический класс GCSettingsSystem.Runtime). GCSettings.IsServerGC расскажет вам, используете ли вы рабочую станцию ​​на серверных версиях, а GCSettings.LatencyMode может сказать вам, используете ли вы параллельные, неконкурентные или специальный, который вы должны установить в коде, который здесь не применим. Я думаю, что это было бы хорошим местом для начала и могло бы объяснить, почему это работает на вашей машине, но не на производстве.

В конфигурационных файлах <gcConcurrent enabled="true|false"/> или <gcServer enabled="true|false"/> управляют режимами сборщика мусора. Имейте в виду, что это может быть в вашем файле app.config(расположенном в стороне от исполняющей сборки) или в файле machine.config, который находится в %windir%\Microsoft.NET\Framework\[version]\CONFIG\

Вы также можете удаленно использовать монитор производительности Windows, чтобы получить доступ к счетчикам производительности производственного компьютера для сбора мусора .NET и просмотреть эти статистические данные. Вы можете сделать то же самое с трассировкой событий для Windows (ETW) удаленно. Для монитора производительности вам нужен объект .NET CLR Memory и выберите ваше приложение в списке экземпляров.