2012-03-12 90 views
28

我有一個WPF應用程序遇到很多性能問題。最糟糕的是,有時候應用程序會在再次運行之前凍結幾秒鐘。在C中監視垃圾收集器#

我目前正在調試應用程序,看看這個凍結可能與什麼有關,我相信可能導致它的一件事是垃圾收集器。由於我的應用程序運行在非常有限的環境中,因此我相信垃圾收集器可以在運行時使用機器的所有資源,並且不會對我們的應用程序造成任何影響。

爲了檢查這個假設,我發現了這些文章:Garbage Collection NotificationsGarbage 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。

更新1

更新我GCMonitor測試與AllocationTest方法。此方法僅用於測試目的。我只是想確保分配足夠的內存來強制垃圾收集器運行。

UPDATE 2

更新了DoGCMonitoring方法,對所述方法和WaitForFullGCApproach WaitForFullGCComplete返回新的檢查。從我目前看到的應用程序直接進入(s == GCNotificationStatus.NotApplicable)條件。所以我認爲我在某個地方有一些錯誤配置,導致我無法獲得理想的結果。

GCNotificationStatus枚舉的文檔可以找到here

+3

您是否嘗試過使用工具實際分析它,比如說Windows性能監視器或windbg--而不是嘗試寫一個GC包裝器? – 2012-03-12 15:53:02

+0

也許GC未運行(尚未)。你能顯示AllocationTest()嗎? – 2012-03-12 15:55:31

+0

嗨,我其實有一個分析工具,但是我之前提到的凍結問題發生在生產環境中,而不是在我的機器上(我無法再現它)。不幸的是,對我來說,我無法在生產環境中運行分析工具。 – Felipe 2012-03-12 15:59:31

回答

38

我看不到GC.RegisterForFullGCNotification(int,int)代碼中的任何地方。看起來您正在使用WaitForFullGC[xxx]方法,但從未註冊通知。這可能就是你獲得NotApplicable狀態的原因。但是,我懷疑GC是你的問題,雖然可能,但我認爲了解所有GC模式以及確定發生的最佳方法是很好的。 .NET中有兩種垃圾收集模式:服務器和工作站。他們都收集了相同的未使用的內存,但是它的做法有些微不同。

  • 服務器版本 - 此模式道出您使用的是服務器端應用的GC,並嘗試優化這些方案的集合。它會將堆分成幾個部分,每個CPU 1個。當GC啓動時,它將在每個CPU上並行運行一個線程。你真的想要多個CPU來運行。雖然服務器版本爲GC使用多個線程,但與下面列出的併發工作站GC模式不同。每個線程的行爲都像非併發版本。

  • 工作站版本 - 此模式告訴GC您正在使用客戶端應用程序。它表明你擁有比Server版本更有限的資源,所以只有一個GC線程。但是,Workstation版本有兩種配置:併發和非併發。

    • 併發 - 這是無論何時使用工作站GC版本默認開啓(這將是您的WPF應用程序的情況下)。 GC始終運行在單獨的線程上,該線程始終在應用程序運行時標記用於收集的對象。此外,它選擇是否在某些代中壓縮內存,並根據性能做出選擇。如果壓縮完成,仍然需要凍結所有線程才能運行集合,但在使用此模式時幾乎不會看到沒有響應的應用程序。這爲用戶創造了更好的交互體驗,最適合於控制檯或GUI應用程序。
    • 非並行 - 這是一個可以配置應用程序以供使用的版本,如果您願意的話。在這種模式下,GC線程會一直啓動,直到GC啓動,然後它會將所有對象樹標記爲垃圾,釋放內存,並在所有其他線程掛起時釋放內存,並將其壓縮。這可能會導致應用程序有時會在一段時間內無響應。

不能註冊爲在併發收集器的通知,因爲這是在後臺完成。有可能您的應用程序沒有使用併發收集器(我注意到您在app.config中禁用了gcConcurrent,但它似乎只用於測試?)。如果是這樣的話,如果收藏量很大,你肯定會看到你的應用程序被凍結。這就是他們創建併發收集器的原因。 GC模式的類型可以部分在代碼中設置,並在應用程序配置和機器配置中完全設置。

我們可以做什麼來弄清楚我們的應用程序正在使用什麼?在運行時,您可以查詢靜態GCSettings類(在System.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 2012-03-12 17:54:23