2012-03-12 102 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