在C#中监视垃圾回收器

34
我有一个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();
    }
}

我已经在我的app.config文件中添加了gcConcurrent选项(如下):

<?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语句的内容从未被执行。
我做错了什么?
注意:我正在使用带有WPF和.NET Framework 3.5的C#。
更新1
使用AllocationTest方法更新了我的GCMonitor测试。此方法仅用于测试目的。我只想确保分配了足够的内存以强制运行垃圾回收器。
更新2
使用WaitForFullGCApproach和WaitForFullGCComplete方法的返回值更新了DoGCMonitoring方法。到目前为止,我看到我的应用程序直接进入(s == GCNotificationStatus.NotApplicable)状态。因此,我认为我在某个地方配置错误,导致无法获得所需的结果。
GCNotificationStatus 枚举类型的文档可以在 此处 找到。

3
你尝试过使用工具进行分析吗?比如Windows性能监视器或Windbg,而不是试图编写GC包装器? - Bryan Crosby
也许垃圾回收器还没运行(但)。你能展示AllocationTest()吗? - H H
您好,我确实有一个分析工具,但是我之前提到的冻结问题发生在生产环境中,而不是在我的机器上(我无法复现)。不幸的是,我无法在生产环境中运行分析工具。 - Felipe
1
@Vilx- 这在技术上是不正确的。服务器GC(例如在ASP.NET中使用)是一种停止整个进程的垃圾回收器。工作站GC(用于WPF和大多数其他桌面应用程序)是一种并发GC-它不会停止整个进程。 - Chris Shain
.NET 并发 GC 和 JVM 并发 GC 一样,在初始标记阶段仍然会停止整个程序。据我所知,唯一没有堆比例停顿的 x86 是 Azul Zing。您可以在此处阅读有关并发 JVM 中停顿的更多信息... http://blog.griddynamics.com/2011/06/understanding-gc-pauses-in-jvm-hotspots_02.html - David Jeske
显示剩余3条评论
1个回答

52

我在你的代码中没有看到 GC.RegisterForFullGCNotification(int,int),看起来你正在使用 WaitForFullGC[xxx] 方法,但是却没有注册通知。这可能是为什么你会得到不适用的状态。

然而,我怀疑 GC 不是你的问题,虽然有可能,但是我认为了解 .NET 中所有的 GC 模式以及确定发生的情况的最佳方式可能会很有帮助。.NET 有两种垃圾回收模式:服务器模式和工作站模式。它们都收集相同的未使用内存,但是做法略有不同。

  • 服务器版本 - 这种模式告诉 GC 你正在使用服务器端应用程序,并尝试优化这些场景的收集。它将堆分成几个部分,每个 CPU 一个。当启动 GC 时,它将在每个 CPU 上并行运行一个线程。你真的需要多个 CPU 才能使其良好工作。虽然服务器版本使用多个线程进行 GC,但它与下面列出的并发工作站 GC 模式不同。每个线程像非并发版本一样运行。

  • 工作站版本 - 这种模式告诉 GC 你正在使用客户端应用程序。它认为你的资源比服务器版本更有限,因此只有一个 GC 线程。然而,工作站版本有两个配置:并发和非并发。

  • 并发模式 - 这是默认情况下工作站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文件(位于执行程序旁边)机器配置文件中,该文件位于%windir% \Microsoft.NET\Framework\[version]\CONFIG\中。

    您还可以远程使用Windows性能监视器来访问生产计算机的.NET垃圾收集器性能计数器并查看这些统计信息。您可以使用Windows事件跟踪(ETW)执行相同的操作,并且这些都可以远程进行。对于性能监视器,您需要.NET CLR Memory对象,并在实例列表框中选择应用程序。


1
嗨,我确实忘记了GC.RegisterForFullGCNotification(int,int)这一行代码...现在感觉有点傻。不管怎样,非常感谢你详细的回答! - Felipe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接