【问题标题】:Gen2 collection not always collecting dead objects?Gen2 收集不总是收集死物?
【发布时间】:2013-12-27 02:30:18
【问题描述】:

通过在过去几天监视一个全新的 .NET 4.5 服务器应用程序的 CLR #Bytes in all Heaps 性能计数器,我注意到一种模式,让我认为 Gen2 收集并不总是收集死对象,但我遇到了麻烦了解到底发生了什么。

服务器应用程序正在 .NET Framework 4.5.1 中使用服务器 GC/后台运行。

这是作为 Windows 服务托管的控制台应用程序(借助 Topshelf 框架)

服务器应用程序正在处理消息,并且吞吐量目前相当稳定。

查看CLR #Bytes in all Heaps 的图表我可以看到,内存开始时约为 18MB,然后在大约 20-24 小时内增长到 35MB(在此时间范围内有 20-30 次 Gen2 集合),然后全部突然回落到 18MB 的标称值,然后在 20-24 小时内再次增长到 ~35MB,然后回落到 18MB,依此类推(我可以看到在过去 6 天应用程序正在运行的模式重复出现) ...内存的增长不是线性的,增长 10MB 大约需要 5 个小时,然后剩下的 10MB 大约需要 15-17 个小时。

问题是我可以通过查看#Gen0/#Gen1/#Gen2 collections 的性能计数器看到,在 20 到 24 小时期间(可能大约 30 小时)发生了一堆 Gen2 收集,并且它们都没有使内存回落到正常值18MB。 然而,奇怪的是通过使用外部工具强制 GC(在我的情况下为 Perfview),然后我可以看到 #Induced GC 上升 1(GC.Collect 被调用,所以这是正常的)并且内存立即消失回到标称的 18MB。

这让我想到 #Gen2 集合的性能计数器不正确,并且在 20-22 小时左右后仅发生一个 Gen2 集合(我真的不这么认为)或者 Gen2 集合不总是收集死对象(似乎更合理)......但在那种情况下,为什么要通过 GC.Collect 强制 GC 来解决问题,显式调用 GC.Collect 与在生命周期内自动触发的收集有什么区别应用程序。

我确信有一个很好的解释,但是从我发现的关于 GC 的不同文档来源 - 太少了 :( - Gen2 集合在任何情况下都会收集死对象。所以也许文档不是最新的或我看错了...欢迎任何解释。谢谢!

编辑:请查看#Bytes in all heaps 4 天内的图表截图

(点击查看大图)

这比试图在脑海中绘制图表更容易。您可以在图表上看到我在上面所说的内容...内存在 20-24 小时内增加(以及在该时间范围内 20-30 次 Gen2 集合),直到达到约 35MB,然后突然下降。您会注意到,在图表的末尾,我通过外部工具触发了诱导 GC,立即将内存回退到标称值。

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

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

【问题讨论】:

  • 您确定您的应用不会泄漏内存吗?
  • 存在第三种可能性,即第二代集合中的对象在您认为它们已死时并未死。我会使用分析工具来跟踪对象的生命周期,看看你是否有一些长期存在的对象(可以存活 20-22 小时)在不应该存在的时候保留引用。此外,您从未说过大型收集之前和之后的对象计数,您是否有一个需要 20 小时才能释放的 17MB 对象,或者您是否有数百个小对象由于一个长期存在的对象而保持活动状态对所有这些的引用。
  • @Scott :是的,但在这种情况下,为什么诱导 GC 会收集这些对象并返回到标称内存,而在应用程序的生命周期内正常的 Gen2 GC 不会这样做。请参阅附图以获得更好的理解。
  • @usr :似乎我有大约 1MB / 24 小时的内存泄漏,我正在对此进行调查,但似乎与我在图表上看到的内容无关。
  • 您的应用程序池的回收频率是多少?

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


【解决方案1】:

来自

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

垃圾回收的条件

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

  • 系统物理内存不足。

  • 托管堆上分配的对象使用的内存超过了可接受的阈值。这个阈值是连续的 在进程运行时进行调整。

  • 调用 GC.Collect 方法。在几乎所有情况下,您都不必调用此方法,因为垃圾收集器会运行 不断地。此方法主要用于特殊情况和 测试。

您似乎正在达到第 2 个,而 35 是阈值。如果 35 太大,您应该能够将阈值配置为其他值。


gen2 集合并没有什么特别之处会导致它们偏离这些规则。 (参见https://stackoverflow.com/a/8582251/215752

【讨论】:

  • 谢谢霍根。我知道这些规则。但是据我了解,这些规则说明了集合的触发条件(无论是 Gen0/Gen1 还是 Gen2,它们都有预算(又名阈值))。一旦触发了集合,它就会收集死对象(如果存在终结器,则等待下一个集合)。因此,如果 GC 决定我的 Gen2 阈值为 35MB,那么它只会在达到阈值时触发 Gen2 GC。所以这意味着根据我的图表,Gen2 收集只会每 20-24 小时发生一次。然而,在此期间,GC 触发了大约 20-30 次 Gen2 收集。
  • @darkey - 我想我可能会因为看不到图像而受到阻碍(这里是防火墙),我回家后会看看它们,也许我会明白的。跨度>
  • @darkey - 该图看起来像每 20-24 小时触发一次集合,这是您所期望的。为什么你认为还有更多?
  • 如果您阅读我的帖子,您会发现在每 20-24 小时内,大约有 20-30 次 Gen 2 收集发生。当您在一个时期结束时看到下降是由于 Gen2 收集。现在我的问题是为什么在此期间之前的 20-30 次 Gen2 集合不回收内存,我预计会有一条平坦的线......这不是因为对象存活了 20-24 小时,因为正如你所见在图的末尾,如果我在周期开始时通过外部工具(Perfview)手动触发诱导 GC(GC.Collect),所有内存都会被 Gen2 回收。
  • 因为我监控“# Gen 2 Collections”性能计数器,并且我知道 20-30 Gen2 发生在一段时间内。来自 MSDN:“显示自应用程序启动以来第 2 代对象被垃圾收集的次数。计数器在第 2 代垃圾收集(也称为完整垃圾收集)结束时递增。” ...考虑到该图表示“# Bytes in all heaps”(“此计数器指示在垃圾收集堆上以字节为单位分配的当前内存。”),除了 GC 之外,我不知道任何其他可以释放它的东西记忆。
【解决方案2】:

您的任何对象都是“大”对象吗?有一个单独的“大对象堆”,它有不同的规则

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

【讨论】:

  • 我有一些大对象,但 LOH 的内存占用非常小(4MB 以某种方式恒定)。此外,当我手动诱导 GC 时,如图所示,从 Gen2 堆大小中删除的内存在手动诱导 GC 后变得更小。然而,使用自动 Gen2 收集(整个图上大约 80-90 个,每小时 20-30 个),我们可以看到 Gen2 堆大小的下降仅在 Gen2 收集后每 20-24 小时发生一次,而不是在每个 Gen2 之后收集
【解决方案3】:

如果启用了gcTrimCommitOnLowMemory,这很容易解释。通常,GC 会为进程保留一些额外的内存。但是,当内存达到某个阈值时,GC 会“修剪”额外的内存。

来自文档:

启用 gcTrimCommitOnLowMemory 设置后,垃圾收集器会评估系统内存负载,并在负载达到 90% 时进入修剪模式。它保持微调模式,直到负载下降到 85% 以下。

这可以很容易地解释您的情况 - 内存储备被保留(并使用)直到您的应用程序达到某个点,这似乎是每 20-24 小时一次,此时检测到 90% 的负载,并且内存被修剪到其最低要求(18mb)。

【讨论】:

  • 嗨里德,感谢您的回答。这确实很有意义,但是这是一个 asp.net 功能,我们不依赖它。该应用程序是作为 Windows 服务托管的纯控制台项目。然而,由于偏执,我检查了所有配置文件,并没有在任何地方看到此配置参数。
  • @Darkey 感谢您的更新 - 如果我想到其他潜在的替代方案,我会考虑一下,明天再编辑...不过,一般来说,35mb 的使用量并不多
  • 确实 35MB 的使用量并不大;)我并没有被内存消耗本身所吸引,而是非常沮丧,因为不了解幕后发生的事情导致这种奇怪的内存回收模式。 b.t.w,我们现在已经解决了这个问题,我在帖子中添加了一个编辑(最后)。
【解决方案4】:

阅读您的第一个版本我会说这是一种正常行为。

...但是在那种情况下,为什么要通过 GC.Collect 强制进行 GC 呢? 技巧,显式调用之间有什么区别 GC.Collect,v.s 生命周期内的自动触发收集 应用程序。

有两种类型的集合,完整集合和部分集合。自动触发所做的是部分收集,但在调用 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

如你所见,只有在对象所在的那一代进行collection时,才会回收带有finalizer的对象的内存。

【讨论】:

  • 感谢您的回答布鲁诺。对于部分/完整,我认为没有“部分”集合之类的东西。 Complete/Full GC 相当于任何 Gen2 收集(来自 MSDN:“第 2 代垃圾收集也称为完全垃圾收集,因为它回收所有代中的所有对象”)。所以调用 GC.Collect() 与自动触发的 Gen2 集合之间应该没有区别。关于可终结的类型,确实需要清理两次 GC(如果不恢复),但考虑到大约 20-30 次 Gen2 GC 在 20-22 小时内发生,它们应该被回收。
  • 我无法理解为什么这么少的内存会如此频繁地触发 Gen2。你确定有 ~20-30 #2 Gen 吗?
  • @darkey有部分收藏,请到msdn.microsoft.com/en-us/library/ms973837.aspx
  • 我在大约同一时期用 Gen2 集合图编辑了我的帖子(由于某种原因,缩放不再起作用,所以只需复制 img url 并粘贴到浏览器中以显示更大)。对于部分收集,我认为这是指 Gen0/Gen1 收集(不是由 Gen2 触发),它只回收部分空间(仅用于 gen)。而一个完整的 GC(Gen2)收集所有的 Gen(包括 0 和 1)
【解决方案5】:

只是想我会在这里投入我的 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());

}

还有一篇很好的文章here 介绍了使用GC.RegisterForFullGCNotification 方法。显然,这将使您还可以包括完整收集的时间跨度,以便您可以根据您的特定需求调整性能和收集频率。此方法还允许您指定一个堆阈值以触发通知(或集合?)。

也可能有一种方法可以在应用程序的 .config 文件中进行设置,但我没有看过。对于现在的服务器应用程序来说,35MB 的占用空间非常小。哎呀,我的网络浏览器有时会达到 300-400MB :) 因此,框架可能只是将 35MB 视为释放内存的一个很好的默认点。

无论如何,我可以从你的问题的深思熟虑中看出,我可能只是指出了显而易见的问题。但是,感觉值得一提。祝你好运!

有趣的笔记

在这篇文章的顶部,我最初写的是“如果(你使用的是 64 位平台)”。这让我崩溃了。保重!

【讨论】:

    【解决方案6】:

    我的 WPF 应用程序中的情况完全相同。顺便说一句,我的代码中没有终结器。然而,正在进行的 GC 似乎实际上收集了 Gen 2 对象。我可以看到触发 Gen2 收集后 GC.GetTotalMemory() 结果最多减少了 150mb。

    所以我的印象是 Gen2 堆大小不显示活动对象使用的字节数。它只是为 Gen2 目的分配的堆大小或字节数。那里可能有大量可用内存。

    在某些情况下(不是在每个 gen 2 集合上)此堆大小会被修剪。在这个特定的时刻,我的应用程序的性能受到了巨大的影响——它可能会挂起几秒钟。想知道为什么...

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-31
      • 1970-01-01
      • 2015-05-01
      • 1970-01-01
      相关资源
      最近更新 更多