2012-03-12 2 views
28

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

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

Чтобы проверить эти гипотезы, я нашел следующие статьи: Garbage Collection Notifications и Garbage Collection Notifications in .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 никогда не выполняется.

Что я делаю неправильно?

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

UPDATE 1

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

UPDATE 2

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

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

+3

Вы пытались на самом деле профилировать его инструментом, сказать что-то вроде монитора производительности Windows или windbg - вместо того, чтобы писать GC-обертку? –

+0

Может быть, GC не работает (пока). Вы можете показать AllocationTest()? –

+0

Привет, У меня действительно есть инструмент профилирования, однако проблема заморозки, о которой я упоминал ранее, происходит в рабочей среде, а не на моей машине (я не могу ее воспроизвести). И, к сожалению, для меня я не могу запустить инструмент профилирования в производственной среде. – Felipe

ответ

38

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

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

  • Server Version - Этот режим говорит 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 (расположенном в стороне от исполняющей сборки) или в машине.config, который находится в %windir%\Microsoft.NET\Framework\[version]\CONFIG\

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

+1

Привет, я сделал действительно забудьте о GC.RegisterForFullGCNotification (int, int) ... Я чувствую себя немного глупо сейчас. В любом случае спасибо за ваш полный ответ! – Felipe

Смежные вопросы