问题描述
我有一个 WPF 应用程序,它遇到了很多性能问题.其中最糟糕的是,有时应用程序会在再次运行之前冻结几秒钟.
I have a WPF application that is experiencing a lot of performance issues. The worst of them is that sometimes the application just freezes for a few seconds before running again.
我目前正在调试应用程序以查看此冻结可能与什么有关,并且我认为可能导致它的原因之一是垃圾收集器.由于我的应用程序运行在一个非常有限的环境中,我相信垃圾收集器在运行时可以使用机器的所有资源,而不会为我们的应用程序留下任何资源.
I am currently debugging the application to see what this freeze might be related to, and I believe that one of things that may be causing it is the Garbage Collector. Since my application is running in a very limited environment, I believe that the Garbage Collector can be using all of the machine's resources when it is ran and leaving none to our application.
为了验证这个假设,我找到了这些文章:垃圾收集通知和 .NET 4.0 中的垃圾收集通知,解释了如何在垃圾收集器开始运行和完成时通知我的应用程序.
To check this hypotheses I found these articles: Garbage Collection Notifications and Garbage Collection Notifications in .NET 4.0, that explain how my application can be notified when the Garbage Collector will begin running and when it is finished.
因此,根据这些文章,我创建了下面的类来获取通知:
So, based on those articles I created the class below to get the notifications:
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 文件(如下):
And I've added the gcConcurrent option to my app.config file (below):
<?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 语句的内容永远不会执行.
However, whenever the application is executed, it seems as if no notification is sent that the Garbage Collector will run. I've put breakpoints in the DoGCMonitoring and it appears that the conditions (s == GCNotificationStatus.Succeeded) and (s == GCNotificationStatus.Succeeded) are never satisfied, therefore the contents of those ifs statements are never executed.
我做错了什么?
注意:我将 C# 与 WPF 和 .NET Framework 3.5 结合使用.
Note: I am using C# with WPF and the .NET Framework 3.5.
更新 1
使用 AllocationTest 方法更新了我的 GCMonitor 测试.此方法仅用于测试目的.我只是想确保分配了足够的内存来强制垃圾收集器运行.
Updated my GCMonitor test with the AllocationTest method. This method is for testing purposes only. I just wanted to make sure that enough memory was being allocated to force the Garbage Collector to run.
更新 2
更新了 DoGCMonitoring 方法,对方法 WaitForFullGCApproach 和 WaitForFullGCComplete 的返回进行了新的检查.从我目前所见,我的应用程序直接进入 (s == GCNotificationStatus.NotApplicable) 条件.所以我认为我在某处存在一些错误配置,导致我无法获得预期的结果.
Updated the DoGCMonitoring method, with new checks on the return of the methods WaitForFullGCApproach and WaitForFullGCComplete. From what I've seen so far my application is going directly to the (s == GCNotificationStatus.NotApplicable) condition. So I think that I have some misconfiguration somewhere that is stopping me from getting the desired results.
可以找到 GCNotificationStatus 枚举的文档这里.
The documentation for the GCNotificationStatus enum can be found here.
推荐答案
我没有看到 GC.RegisterForFullGCNotification(int,int)
代码中的任何位置.看起来您正在使用 WaitForFullGC[xxx]
方法,但从未注册通知.这可能就是您获得 NotApplicable 状态的原因.
I don't see GC.RegisterForFullGCNotification(int,int)
anywhere in your code. It looks like you're using the WaitForFullGC[xxx]
methods, but are never registering for the notification. That's probably why you're getting the NotApplicable status.
但是,我怀疑 GC 是您的问题,虽然有可能,但我想了解所有 GC 模式以及确定正在发生的事情的最佳方法会很好..NET 中有两种垃圾收集模式:服务器和工作站.它们都收集相同的未使用内存,但其完成方式略有不同.
However, I'm doubting that GC is your problem, while possible, I suppose it would be good to know about all of the GC modes there are and the best ways to determine what is happening. There are two modes of Garbage Collection in .NET: the Server and Workstation. They both collect the same unused memory, however the way it's done is ever so slightly different.
服务器版本 - 此模式告诉 GC 您正在使用服务器端应用程序,并尝试针对这些场景优化集合.它将把堆分成几个部分,每个 CPU 1 个.当 GC 启动时,它会在每个 CPU 上并行运行一个线程.您确实需要多个 CPU 才能正常工作.虽然服务器版本对 GC 使用多线程,但它与下面列出的并发工作站 GC 模式不同.每个线程都像非并发版本.
Server Version - This mode tells the GC for you're using a server side application, and it tries to optimize collections for these scenarios. It will split the heap into several sections, 1 per CPU. When the GC is started, it will run one thread on each CPU in parallel. You really want multiple CPUs for this to work well. While the server version uses multiple threads for the GC, it's not the same as the concurrent workstation GC mode listed below. Each thread acts like the non-concurrent version.
工作站版本 - 此模式告诉 GC 您正在使用客户端应用程序.它认为您的资源比服务器版本更有限,因此只有一个 GC 线程.但是Workstation版本有两种配置:并发和非并发.
Workstation Version - This mode tells GC you're using a client side application. It figures you have more limited resources than the Server version, and so there is only one GC thread. However, there are two configurations of the Workstation version: concurrent and non-concurrent.
- 并发 - 这是在使用工作站 GC 时默认打开的版本(WPF 应用程序就是这种情况).GC 始终在单独的线程上运行,该线程始终在应用程序运行时标记要收集的对象.此外,它选择是否在某些代中压缩内存,并根据性能做出选择.如果压缩完成,它仍然必须冻结所有线程以运行集合,但是在使用此模式时,您几乎永远不会看到无响应的应用程序.这为使用创造了更好的交互体验,最适合控制台或 GUI 应用.
- 非并发 - 如果您愿意,可以配置应用程序使用此版本.在这种模式下,GC 线程休眠直到 GC 启动,然后它会标记所有垃圾对象树,释放内存,并压缩它,同时所有其他线程都被挂起.这会导致应用程序有时在很短的时间内没有响应.
- Concurrent - This is the version turned on by default whenever the workstation GC is used (this would be the case for your WPF application). The GC is always running on a separate thread that is always marking objects for collection when the application is running. Furthermore, it chooses whether or not to compact the memory in certain generations, and makes that choice based on performance. It still has to freeze all threads to run a collection if compacting is done, but you will almost never see an unresponsive application when using this mode. This creates a better interactive experience for uses and is best for console or GUI apps.
- Non-Concurrent - This is a version you can configure your application to use, if you'd like. In this mode, the GC thread sleeps until a GC is started, then it goes and marks all object trees that are garbage, frees the memory, and compacts it, all while all other threads are suspended. This can cause application to sometimes become unresponsive for a short period of time.
您无法在并发收集器上注册通知,因为这是在后台完成的.您的应用程序可能没有使用并发收集器(我注意到您在 app.config
中禁用了 gcConcurrent
,但这似乎仅用于测试?).如果是这种情况,如果有大量收集,您当然可以看到您的应用程序冻结.这就是他们创建并发收集器的原因.GC模式的类型可以部分在代码中设置,完全在应用配置和机器配置中设置.
You can't register for notifications on the concurrent collector, since that's done in the background. It's possible that your application is not using the concurrent collector (I notice you have the gcConcurrent
disabled in the app.config
, but it seems that's only for testing?). If that's the case, you can certainly see your application freezing if there's heavy collections. This is why they created the concurrent collector. The type of GC mode can partially be set in code, and fully set in application configurations and machine configurations.
我们可以做些什么来准确地弄清楚我们的应用程序正在使用什么?在运行时,您可以查询静态GCSettings
类(在System.Runtime
中).GCSettings.IsServerGC
会告诉您是否在服务器版本和 GCSettings.LatencyMode
可以告诉您是使用并发、非并发还是特殊的,您必须在代码中设置它在这里并不真正适用.我认为这是一个很好的起点,并且可以解释为什么它在您的机器上运行良好,但在生产环境中却没有.
What can we do to figure out exactly what our application is using? At runtime, you can query the static GCSettings
class (in System.Runtime
). GCSettings.IsServerGC
will tell you if you're running the workstation on server versions and GCSettings.LatencyMode
can tell you if you're using the concurrent, non-concurrent or a special one you have to set in code which isn't really applicable here. I think that would be a good place to start, and could explain why it's running fine on your machine, but not production.
在配置文件中,<gcConcurrent enabled="true|false"/>
或 <gcServer enabled="true|false"/>
控件垃圾收集器的模式.请记住,这可以在您的 app.config 文件中(位于执行程序集旁边)或在 machine.config 文件中,该文件位于 %windir%Microsoft.NETFramework[版本]CONFIG
In the configuration files, <gcConcurrent enabled="true|false"/>
or <gcServer enabled="true|false"/>
control the modes of the garbage collector. Keep in mind this can be in your app.config file (located aside the executing assembly) or in the machine.config file, which is located in %windir%Microsoft.NETFramework[version]CONFIG
您还可以远程使用 Windows 性能监视器来访问生产机器的 .NET 垃圾收集性能计数器并查看这些统计信息.您可以远程使用 Windows 事件跟踪 (ETW) 执行相同操作.对于性能监视器,您需要 .NET CLR Memory
对象,然后在实例列表框中选择您的应用程序.
You can also remotely use the Windows Performance Monitor to access the production machine's performance counters for .NET garbage collection and view those statistics. You can do the same with Event Tracing for Windows (ETW) all remotely. For performance monitor, you'd want the .NET CLR Memory
object, and select your application in the instances list box.
这篇关于在 C# 中监控垃圾收集器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!