Gen2集合并不总是收集死对象?

时间:2013-12-09 20:13:34

标签: c# .net garbage-collection .net-4.5

通过监控过去几天全新.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图表的此屏幕截图

(点击查看大图)
graph

这比试图描绘你头脑中的东西更容易。您在图表上看到的就是我上面所说的...内存增加超过20-24小时(以及在此时间范围内20-30 Gen2集合),直到达到~35MB然后突然下降。您将在图表的末尾注意到,通过外部工具触发的诱导GC I会立即将内存降至标称值。

编辑#2:我在代码中做了很多清理工作,主要是关于终结器。我有很多关于一次性类型的类,所以我必须在这些类型上实现IDisposable。然而,在任何情况下,我都被一些文章误导为使用Finalizer实现Diposable模式。在阅读了一些MSDN文档之后,我开始明白只有当类型本身拥有本机资源时才需要终结器(在这种情况下,使用SafeHandle可以避免这种情况)。所以我从所有这些类型中删除了所有终结器。代码中还有一些其他的修改,但主要是业务逻辑,没有“.NET框架”相关。 现在图表非常不同,这是一个平坦的线路,现在已经持续了20天......正是我期待看到的! 所以这个问题现在已经解决了,但是我仍然不知道是什么问题导致......似乎它可能与终结器有关,但仍然无法解释我注意到的内容,即使我们没有调用Dispose (true) - 压缩终结器 - 终结器线程应该在收集之间启动而不是每20-24小时? 考虑到我们现在已经摆脱了问题,需要时间回到“越野车”版本并再次重现它。我可能会尝试做一段时间,然后深入了解它。

编辑:添加了Gen2集合图(点击查看大图)

graph

6 个答案:

答案 0 :(得分:7)

来自

http://msdn.microsoft.com/en-us/library/ee787088%28v=VS.110%29.aspx#workstation_and_server_garbage_collection

  

垃圾收集的条件

     

当满足下列条件之一时,就会发生垃圾收集   真:

     
      
  • 系统物理内存较低。

  •   
  • 托管堆上已分配对象使用的内存超过了可接受的阈值。这个门槛是连续的   随着流程的进行调整。

  •   
  • 调用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代集合),此堆大小将被修剪。在这个特殊的时刻,我的应用程序获得了巨大的性能损失 - 它可能会挂起几秒钟。想知道为什么......