通过监控过去几天全新.NET 4.5服务器应用程序的CLR #Bytes in all Heaps
性能计数器,我可以注意到一种模式让我觉得Gen2集合并不总是收集死对象,但我是无法理解究竟发生了什么。
服务器应用程序使用Server GC / Background在 .NET Framework 4.5.1中运行。
这是一个托管为Windows服务的控制台应用程序(在Topshelf框架的帮助下)
服务器应用程序正在处理消息,并且吞吐量在某种程度上是不变的。
我可以看到CLR #Bytes in all Heaps
的图表是内存开始大约为18MB,然后在大约20-24小时内增长到35MB(在此时间范围内有20-30个Gen2集合),然后突然回落到18MB的标称值,然后在20-24小时内再次增长到~35MB,然后又回落到18MB,等等(我可以看到这个模式在过去的6天内重复了应用程序现在运行)...内存的增长不是线性的,大约需要5个小时才能增长10MB,然后剩下的10 MB大约需要15-17个小时。
我可以通过查看#Gen0/#Gen1/#Gen2 collections
的perfmon计数器来看到,在20-24小时内(可能是30周年纪念日),一堆Gen2集合正在进行,并且没有一个会让内存回落标称18MB。
然而,奇怪的是通过使用外部工具强制GC(在我的情况下为Perfview),然后我可以看到#Induced GC
上升1(GC.Collect被调用,所以这是正常的)并立即记忆将回归名义上的18MB。
这让我想到,#Gen2集合的perfmon计数器不对,只有一个Gen2集合在20-22小时后发生(meeehhh我真的不这么认为)或者Gen2集合没有总是收集死对象(似乎更合理)...但在这种情况下为什么会通过GC.Collect强制GC做伎俩,明确调用GC.Collect与自动触发的集合在生命周期中的区别是什么申请。
我确信有一个非常好的解释但是从我发现的关于GC的文档的不同来源很少:( - Gen2集合在任何情况下都会收集死对象。所以也许文档不是最新的或者我误读了......欢迎任何解释。谢谢!
编辑:请在4天内查看#Bytes in all heaps
图表的此屏幕截图
(点击查看大图)
这比试图描绘你头脑中的东西更容易。您在图表上看到的就是我上面所说的...内存增加超过20-24小时(以及在此时间范围内20-30 Gen2集合),直到达到~35MB然后突然下降。您将在图表的末尾注意到,通过外部工具触发的诱导GC I会立即将内存降至标称值。
编辑#2:我在代码中做了很多清理工作,主要是关于终结器。我有很多关于一次性类型的类,所以我必须在这些类型上实现IDisposable
。然而,在任何情况下,我都被一些文章误导为使用Finalizer实现Diposable模式。在阅读了一些MSDN文档之后,我开始明白只有当类型本身拥有本机资源时才需要终结器(在这种情况下,使用SafeHandle可以避免这种情况)。所以我从所有这些类型中删除了所有终结器。代码中还有一些其他的修改,但主要是业务逻辑,没有“.NET框架”相关。
现在图表非常不同,这是一个平坦的线路,现在已经持续了20天......正是我期待看到的!
所以这个问题现在已经解决了,但是我仍然不知道是什么问题导致......似乎它可能与终结器有关,但仍然无法解释我注意到的内容,即使我们没有调用Dispose (true) - 压缩终结器 - 终结器线程应该在收集之间启动而不是每20-24小时?
考虑到我们现在已经摆脱了问题,需要时间回到“越野车”版本并再次重现它。我可能会尝试做一段时间,然后深入了解它。
编辑:添加了Gen2集合图(点击查看大图)
答案 0 :(得分:7)
来自
垃圾收集的条件
当满足下列条件之一时,就会发生垃圾收集 真:
系统物理内存较低。
托管堆上已分配对象使用的内存超过了可接受的阈值。这个门槛是连续的 随着流程的进行调整。
调用GC.Collect方法。几乎在所有情况下,您都不必调用此方法,因为垃圾收集器会运行 不断。此方法主要用于独特的情况和 测试
看来你正在击中第二个,35是门槛。如果35很大,您应该可以将阈值配置为其他值。
gen2集合没有任何特殊之处会导致它们偏离这些规则。 (cf https://stackoverflow.com/a/8582251/215752)
答案 1 :(得分:1)
你的任何物体都是“大”物体吗?有一个单独的“大对象堆”,它有不同的规则
Large Object Heap Fragmentation
但在4.5.1中有所改进:
http://blogs.msdn.com/b/dotnet/archive/2011/10/04/large-object-heap-improvements-in-net-4-5.aspx
答案 2 :(得分:1)
如果启用gcTrimCommitOnLowMemory
,则可以很容易地解释这一点。通常,GC会为进程分配一些额外的内存。但是,当内存达到某个阈值时,GC将“修剪”额外的内存。
来自文档:
当启用gcTrimCommitOnLowMemory设置时,垃圾收集器会评估系统内存负载,并在负载达到90%时进入修剪模式。它保持修整模式,直到负载降至85%以下。
这可以很容易地解释您的情况 - 在您的应用程序达到某个点之前保留(和使用)内存储备,这似乎是每20-24小时一次,此时检测到90%的负载,并且内存被修剪到最低要求(18mb)。
答案 3 :(得分:1)
阅读你的第一个版本我会说这是一种正常行为。
...但在那种情况下,为什么会通过GC.Collect强制GC执行 诀窍,明确呼唤之间会有什么区别 GC.Collect,v。在生命周期内自动触发的集合 申请。
有两种类型的集合,完整集合和部分集合。自动触发的功能是部分集合,但在调用GC.Collect时,它将执行完整集合。
与此同时,我可能有理由告诉我们您在所有对象上都使用了终结器。如果出于任何原因,其中一个对象被提升为#2 Gen,则终结器仅在执行#2 Gen集合时运行。
以下示例将演示我刚才所说的内容。
public class ClassWithFinalizer
{
~ClassWithFinalizer()
{
Console.WriteLine("hello from finalizer");
//do nothing
}
}
static void Main(string[] args)
{
ClassWithFinalizer a = new ClassWithFinalizer();
Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
GC.Collect();
Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
GC.Collect();
Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
a = null;
Console.WriteLine("Collecting 0 Gen");
GC.Collect(0);
GC.WaitForPendingFinalizers();
Console.WriteLine("Collecting 0 and 1 Gen");
GC.Collect(1);
GC.WaitForPendingFinalizers();
Console.WriteLine("Collecting 0, 1 and 2 Gen");
GC.Collect(2);
GC.WaitForPendingFinalizers();
Console.Read();
}
输出将是:
Class a is on #0 generation
Class a is on #1 generation
Class a is on #2 generation
Collecting 0 Gen
Collecting 0 and 1 Gen
Collecting 0, 1 and 2 Gen
hello from finalizer
正如您所看到的,只有在对象生成的集合上进行集合时,才会回收具有终结器的对象的内存。
答案 4 :(得分:0)
我想在这里投入2美分。我不是这方面的专家,但也许这可能有助于你的调查。
如果您使用的是64位平台,请尝试将其添加到.config文件中。我读到这可能是一个问题。
<configuration>
<runtime>
<gcAllowVeryLargeObjects enabled="true" />
</runtime>
</configuration>
我要指出的另一件事是,如果您掌握了源代码,可以通过内部故障排除来证明您的假设。
根据你的应用程序的主内存消耗类调用一些东西,并将其设置为按时间间隔运行,可以揭示实际发生的事情。
private void LogGCState() {
int gen = GC.GetGeneration(this);
//------------------------------------------
// Comment out the GC.GetTotalMemory(true) line to see what's happening
// without any interference
//------------------------------------------
StringBuilder sb = new StringBuilder();
sb.Append(DateTime.Now.ToString("G")).Append('\t');
sb.Append("MaxGens: ").Append(GC.MaxGeneration).Append('\t');
sb.Append("CurGen: ").Append(gen).Append('\t');
sb.Append("CurGenCount: ").Append(GC.CollectionCount(gen)).Append('\t');
sb.Append("TotalMemory: ").Append(GC.GetTotalMemory(false)).Append('\t');
sb.Append("AfterCollect: ").Append(GC.GetTotalMemory(true)).Append("\r\n");
File.AppendAllText(@"C:\GCLog.txt", sb.ToString());
}
关于使用GC.RegisterForFullGCNotification
方法,还有一篇非常好的文章here。显然,这将使您能够包括完整集合的时间跨度,以便您可以根据您的特定需求调整性能和收集频率。此方法还允许您指定堆阈值以触发通知(或集合?)。
也许还有一种方法可以在apps .config文件中设置它,但我没看过。目前,对于服务器应用程序来说,35MB的占用空间非常小。哎呀,我的网页浏览器有时会达到300-400MB :)所以,框架可能只看到35MB作为释放内存的一个很好的默认点。
无论如何,我可以通过你的问题的深思熟虑告诉我,我可能只是指出了显而易见的问题。但是,值得一提的是。祝你好运!。
有趣的说明
在这篇文章的顶部,我最初写的是“if(你使用的是64位平台)”。那让我搞砸了。小心!
答案 5 :(得分:0)
我的WPF应用程序中的情况完全相同。我的代码中没有终结器。然而,正在进行的GC似乎实际上收集了Gen 2对象。我可以看到GC.GetTotalMemory()结果在Gen2集合触发后减少了150mb。
因此,我认为Gen2堆大小不显示活动对象使用的字节数。它只是为Gen2目的分配的堆大小或字节数。那里你可能有足够的空闲记忆。
在某些情况下(不是每个第2代集合),此堆大小将被修剪。在这个特殊的时刻,我的应用程序获得了巨大的性能损失 - 它可能会挂起几秒钟。想知道为什么......