草庐IT

c# - 在C#中监视垃圾收集器

coder 2023-07-10 原文

我有一个WPF应用程序,遇到很多性能问题。最糟糕的是,有时应用程序会冻结几秒钟,然后再次运行。

我目前正在调试该应用程序,以查看此冻结可能与之相关,并且我认为可能导致此冻结的原因之一是垃圾收集器。由于我的应用程序在非常有限的环境中运行,因此我相信垃圾收集器在运行时可以使用机器的所有资源,而不会将任何资源留给我们的应用程序。

为了验证这一假设,我找到了以下文章: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语句的内容永远不会执行。

我究竟做错了什么?

注意:我在WPF和.NET Framework 3.5中使用C#。

更新1

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

更新2

更新了DoGCMonitoring方法,并对方法WaitForFullGCApproach和WaitForFullGCComplete的返回进行了新检查。从目前为止我所看到的,我的应用程序将直接进入(s == GCNotificationStatus.NotApplicable)条件。因此,我认为我在某处存在一些配置错误,这使我无法获得所需的结果。

可以在here中找到GCNotificationStatus枚举的文档。

最佳答案

在您的代码中的任何地方都看不到 GC.RegisterForFullGCNotification(int,int) 。看起来您正在使用WaitForFullGC[xxx]方法,但从未注册该通知。这可能就是为什么您获得“不适用”状态的原因。

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

  • 服务器版本-此模式告诉GC您正在使用服务器端应用程序,并尝试针对这些方案优化集合。它将堆分成几个部分,每个CPU 1个。启动GC后,它将在每个CPU上并行运行一个线程。您确实想要多个CPU才能正常工作。服务器版本的GC使用多个线程,但与下面列出的并发工作站GC模式不同。每个线程的行为都类似于非并发版本。
  • 工作站版本-此模式告诉GC您正在使用客户端应用程序。它表明您拥有比Server版本更多的有限资源,因此只有一个GC线程。但是,Workstation版本有两种配置:并发和非并发。
  • 并发-这是在使用工作站GC时默认打开的版本(WPF应用程序就是这种情况)。 GC始终在单独的线程上运行,该线程始终在应用程序运行时标记要收集的对象。此外,它选择是否在某些代中压缩内存,然后根据性能进行选择。如果完成压缩,它仍然必须冻结所有线程才能运行集合,但是使用此模式时,您几乎不会看到无响应的应用程序。这为使用创造了更好的交互体验,最适合控制台或GUI应用。
  • 非并行-您可以根据需要配置该版本以使用您的应用程序。在这种模式下,GC线程一直休眠直到启动GC,然后它去标记所有垃圾对象树,释放内存并压缩它,同时所有其他线程都被挂起。这可能导致应用程序有时在短时间内无响应。

  • 您不能在并发收集器上注册通知,因为这是在后台完成的。您的应用程序可能没有使用并发收集器(我注意到您在gcConcurrent中禁用了app.config,但是似乎仅用于测试吗?)。如果是这种情况,那么在有大量收集的情况下,您当然可以看到应用程序冻结。这就是为什么他们创建并发收集器的原因。 GC模式的类型可以在代码中部分设置,而在应用程序配置和机器配置中完全设置。

    我们该怎么做才能确切地了解我们的应用程序正在使用什么?在运行时,您可以查询静态GCSettings类(在System.Runtime中)。 GCSettings.IsServerGC会告诉您您是否在服务器版本上运行工作站,而 GCSettings.LatencyMode 可以告诉您是否正在使用并发,非并发或特殊的代码,您必须在此处设置的代码并不适用。我认为这将是一个不错的起点,并可以解释为什么它在您的机器上运行正常,但在生产环境上却无法正常运行。

    在配置文件中,<gcConcurrent enabled="true|false"/><gcServer enabled="true|false"/>控制垃圾收集器的模式。请记住,这可以在您的app.config文件中(位于执行程序集旁边)或在%windir%\Microsoft.NET\Framework\[version]\CONFIG\中的machine.config文件中

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

    关于c# - 在C#中监视垃圾收集器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9669963/

    有关c# - 在C#中监视垃圾收集器的更多相关文章

    1. c# - 如何在 ruby​​ 中调用 C# dll? - 2

      如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

    2. C# 到 Ruby sha1 base64 编码 - 2

      我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

    3. 基于C#实现简易绘图工具【100010177】 - 2

      C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

    4. c# - C# 中的 Flatten Ruby 方法 - 2

      我如何做Ruby方法"Flatten"RubyMethod在C#中。此方法将锯齿状数组展平为一维数组。例如:s=[1,2,3]#=>[1,2,3]t=[4,5,6,[7,8]]#=>[4,5,6,[7,8]]a=[s,t,9,10]#=>[[1,2,3],[4,5,6,[7,8]],9,10]a.flatten#=>[1,2,3,4,5,6,7,8,9,10 最佳答案 递归解决方案:IEnumerableFlatten(IEnumerablearray){foreach(variteminarray){if(itemisIEnume

    5. ruby - 可以像在 C# 中使用#region 一样在 Ruby 中使用 begin/end 吗? - 2

      我最近从C#转向了Ruby,我发现自己无法制作可折叠的标记代码区域。我只是想到做这种事情应该没问题:classExamplebegin#agroupofmethodsdefmethod1..enddefmethod2..endenddefmethod3..endend...但是这样做真的可以吗?method1和method2最终与method3是同一种东西吗?还是有一些我还没有见过的用于执行此操作的Ruby惯用语? 最佳答案 正如其他人所说,这不会改变方法定义。但是,如果要标记方法组,为什么不使用Ruby语义来标记它们呢?您可以使用

    6. c# - Ruby 等效于 C# Linq 聚合方法 - 2

      什么是Linq聚合方法的ruby​​等价物。它的工作原理是这样的varfactorial=new[]{1,2,3,4,5}.Aggregate((acc,i)=>acc*i);每次将数组序列中的值传递给lambda时,变量acc都会累积。 最佳答案 这在数学以及几乎所有编程语言中通常称为折叠。它是更普遍的变形概念的一个实例。Ruby从Smalltalk中继承了这个特性的名称,它被称为inject:into:(像aCollectioninject:aStartValueinto:aBlock一样使用。)所以,在Ruby中,它称为inj

    7. ruby - 在 Ruby 数组中收集重复项的最快/单行方法? - 2

      像这样转换数组的最快/单行方法是什么:[1,1,1,1,2,2,3,5,5,5,8,13,21,21,21]...进入像这样的对象数组:[{1=>4},{2=>2},{3=>1},{5=>3},{8=>1},{13=>1},{21=>3}] 最佳答案 要获得所需的格式,您可以附加一个调用以映射到您的解决方案:array.inject({}){|h,v|h[v]||=0;h[v]+=1;h}.map{|k,v|{k=>v}}虽然它仍然是单行的,但它开始变得凌乱了。 关于ruby-在Ruby

    8. c# - 先学什么? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭8年前。Improvethisquestion几年前我去学校学习编程,毕业后我找到了一份系统管理方面的工作,这就是我职业生涯的方向。我想重新开始某种开发,并且一直在“玩”C#和ASP.NET,但我已经听到很多关于其他"new"语言的讨论(新的意思是它们是新的)我)喜欢Ruby和F#。我想我想知道我是否在浪费时间学习主要的MS语言,而不是成为一名通才。很长一段时间没有离开开发社区(如果我曾经离开过的话)让我在潮流中挣扎,我不想落在时代的

    9. c# - 在 C# 中重现 Ruby OpenSSL private_encrypt 输出 - 2

      我有一个简单的Ruby脚本,我用它在某些HTTPheader上执行private_encrypt以签署要发送到ruby​​RESTAPI的Web请求,该API会根据Base64编码字符串测试Base64编码字符串生成而不是解码Base64和解密数据然后测试原始字符串。我使用的脚本是require"openssl"require"base64"path_to_cert=ARGV[0].dupplain_text=Base64.decode64(ARGV[1].dup)private_key=OpenSSL::PKey::RSA.new(File.read(path_to_cert))pu

    10. ruby-on-rails - 为什么 Devise/Omniauth 会向 URL 添加垃圾? - 2

      使用facebook登录后,我被重定向到/#_=_,其中显示主页。这种垃圾也出现在其他URL中,例如当注册失败并被重定向到/users/sign_in#_=_为什么会发生这种情况,我该如何解决? 最佳答案 如果你真的不想要它,一些简单的javascript就可以了:if(window.location.hash=="#_=_"){window.location.hash="";} 关于ruby-on-rails-为什么Devise/Omniauth会向URL添加垃圾?,我们在StackO

    随机推荐