在C#中监视垃圾收集器

Fel*_*ipe 33 .net c# garbage-collection .net-3.5

我有一个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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我已经将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>
Run Code Online (Sandbox Code Playgroud)

但是,无论何时执行应用程序,似乎都没有发送垃圾收集器将运行的通知.我在DoGCMonitoring中放置了断点,似乎从不满足条件(s == GCNotificationStatus.Succeeded)和(s == GCNotificationStatus.Succeeded),因此永远不会执行那些ifs语句的内容.

我究竟做错了什么?

注意:我正在使用带有WPF的C#和.NET Framework 3.5.

更新1

使用AllocationTest方法更新了我的GCMonitor测试.此方法仅用于测试目的.我只是想确保分配足够的内存来强制垃圾收集器运行.

更新2

更新了DoGCMonitoring方法,对WaitForFullGCApproach和WaitForFullGCComplete方法的返回进行了新的检查.从我到目前为止看到的我的应用程序直接进入(s == GCNotificationStatus.NotApplicable)条件.所以我认为我在某处错误配置阻止我获得理想的结果.

可以在此处找到GCNotificationStatus枚举的文档.

Chr*_*ens 41

GC.RegisterForFullGCNotification(int,int)在你的代码中没有看到任何地方.看起来您正在使用这些WaitForFullGC[xxx]方法,但从未注册过通知.这可能就是你获得NotApplicable状态的原因.

但是,我怀疑GC是你的问题,尽管可能,我想最好知道所有GC模式以及确定发生情况的最佳方法..NET中有两种垃圾收集模式:服务器和工作站.它们都收集了相同的未使用内存,但它的工作方式却略有不同.

  • 服务器版本 - 此模式告诉GC您正在使用服务器端应用程序,并尝试优化这些方案的集合.它会将堆分成几个部分,每个CPU 1个.GC启动时,它将在每个CPU上并行运行一个线程.你真的想要多个CPU才能很好地工作.虽然服务器版本为GC使用多个线程,但它与下面列出的并发工作站GC模式不同.每个线程的行为类似于非并发版本.

  • 工作站版本 - 此模式告诉GC您正在使用客户端应用程序.它表明您拥有的资源比服务器版本更有限,因此只有一个GC线程.但是,Workstation版本有两种配置:并发和非并发.

    • 并发 - 这是默认情况下在使用工作站GC时打开的版本(这适用于您的WPF应用程序).GC始终在一个单独的线程上运行,该线程始终在应用程序运行时标记要收集的对象.此外,它选择是否在某些代中压缩存储器,并根据性能做出选择.如果完成压缩,它仍然必须冻结所有线程以运行集合,但在使用此模式时,您几乎不会看到无响应的应用程序.这为使用创建了更好的交互式体验,最适合控制台或GUI应用程序.
    • 非并发 - 如果您愿意,可以配置您的应用程序使用的版本.在这种模式下,GC线程会一直处于休眠状态,直到启动GC,然后它将标记所有对象树,这些树是垃圾,释放内存压缩它,所有其他线程都被挂起.这可能导致应用程序有时在短时间内无响应.

您无法在并发收集器上注册通知,因为这是在后台完成的.您的应用程序可能没有使用并发收集器(我注意到您已经gcConcurrent禁用了app.config,但似乎仅用于测试?).如果是这种情况,如果收藏很多,您肯定可以看到您的应用程序冻结.这就是他们创建并发收集器的原因.GC模式的类型可以部分在代码中设置,并在应用程序配置和机器配置中完全设置.

我们可以做些什么来弄清楚我们的应用程序正在使用什么?在运行时,您可以查询静态GCSettings类(in System.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对象,并在实例列表框中选择您的应用程序.