Мониторинг сборщика мусора в C # - PullRequest
32 голосов
/ 12 марта 2012

У меня есть приложение 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) никогда не выполняются, поэтому содержимое этих операторов if никогда не выполняется.

Что я делаю не так?

Примечание. Я использую C # с WPF и .NET Framework 3.5.

ОБНОВЛЕНИЕ 1

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

ОБНОВЛЕНИЕ 2

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

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

1 Ответ

40 голосов
/ 12 марта 2012

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

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

  • Версия сервера - Этот режим сообщает ГХ о том, что вы используетеприложение на стороне сервера, и оно пытается оптимизировать коллекции для этих сценариев.Он разделит кучу на несколько разделов, по 1 на процессор.Когда 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 Performance Monitor для доступа к счетчикам производительности рабочего компьютера для сбора мусора .NET и просмотра этой статистики.Вы можете сделать то же самое с Event Tracing для Windows (ETW) все удаленно.Для монитора производительности вам нужен объект .NET CLR Memory и выберите свое приложение в окне списка экземпляров.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...