【问题标题】:GC.Collect on only generation 2 & large object heapGC.Collect 仅在第 2 代和大对象堆上
【发布时间】:2009-09-23 22:23:47
【问题描述】:

在我的应用程序中,有一段时间会同时释放大量大型对象。那时我想专门对大对象堆(LOH)进行垃圾收集。

我知道您不能这样做,您必须调用GC.Collect(2),因为只有在进行第 2 代收集时,才会在 LOH 上调用 GC。但是,我在文档中读到调用 GC.Collect(2) 仍会在第 1 代和第 0 代上运行 GC。

是否可以强制 GC收集 gen 2,而不包括 gen 1 或 gen 0?

如果不可能,是否有理由这样设计 GC?

【问题讨论】:

  • 为什么要这样做,即不从 gen 0 或 1 收集? .NET GC 在其自己的设备上运行得最好。
  • 我知道这一点。基本上,您永远不想手动强制 GC,因为它们是密集操作。既然是这样,当我看到需要运行 GC 时,我希望它只针对特定的代运行,而不是执行完整的 GC。我正在尝试对 GC 的使用更加挑剔,但它不允许我这样做。

标签: .net garbage-collection large-object-heap


【解决方案1】:

这是不可能的。 GC 被设计为使第 2 代收集始终同时收集第 0 代和第 1 代。

编辑:在a GC developer's blog找到你的来源:

Gen2 GC 需要完整的收集 (Gen0、Gen1、Gen2 和 LOH!大 对象在每次 Gen2 GC 时进行 GC 即使 GC 不是由 LOH 空间不足。请注意,那里 不是只收集大量的 GC 对象。)这需要比 年轻一代的收藏。

编辑 2:来自同一个博客的 Using GC Efficiently Part 1Part 2 显然 Gen0 和 Gen1 集合比 Gen2 集合快,所以对我来说只做Gen2 不会带来太多性能优势。可能有更根本的原因,但我不确定。或许答案就在该博客的某篇文章中。

【讨论】:

  • 谢谢!请注意,我已经编辑了我的问题,还询问了为什么它会以这种方式受到限制。
【解决方案2】:

由于所有新的分配(除了大对象)总是进入 Gen0,GC 被设计为总是从指定的代及以下进行收集。当您调用GC.Collect(2) 时,您是在告诉 GC 从 Gen0、Gen1 和 Gen2 中收集。

如果您确定要处理大量大对象(在分配时对象大到足以放置在 LOH 上),最好的选择是确保在以下情况下将它们设置为 null(VB 中为无)你已经完成了他们。 LOH 分配尝试智能并重用块。例如,如果您在 LOH 上分配了一个 1MB 的对象,然后将其丢弃并将其设置为 null,那么您将留下一个 1MB 的“洞”。下次您在 LOH 上分配大小为 1MB 或更小 的任何内容时,它将填充该孔(并继续填充它,直到下一次分配太大而无法容纳剩余空间,此时它将分配一个新块。)

请记住,.NET 中的生成不是物理事物,而是有助于提高 GC 性能的逻辑分离。由于所有新分配都进入 Gen0,因此始终是要收集的第一代。每个运行的收集周期,任何在较低代中幸存下来的收集都被“提升”到下一个最高代(直到到达 Gen2)。

在大多数情况下,GC 不需要超越收集 Gen0。目前的 GC 实现可以同时收集 Gen0 和 Gen1,但不能在收集 Gen0 或 Gen1 时收集 Gen2。 (.NET 4.0 大大放宽了这个限制,在大多数情况下,GC 能够收集 Gen2,同时也收集 Gen0 或 Gen1。)

【讨论】:

  • 您的解释很好地概述了 GC 的工作原理,但并未阐明为什么存在排除 严格 gen 1 或 gen 2 集合的约束。
  • 设置myVar = null 没有任何作用。见底部bryancook.net/2008/05/net-garbage-collection-behavior-for.html
  • 我相信原因是只有在 GC 期间评估(和影响)对象到更高代的提升,所以如果你不收集低代,也许没有什么(新)可做。 .
【解决方案3】:

要回答“为什么”这个问题:在物理上,没有 Gen0 和 Gen1 或 Gen2 这样的东西。它们都在虚拟地址空间上使用相同的内存块。它们之间的区别实际上只能通过绕着一个想象的边界界限移动来实现。

每个(小)对象都是从 Gen0 堆区域分配的。如果 - 在收集之后 - 它存活下来,它会“向下”移动到托管堆块的那个区域,最终刚刚从垃圾中释放出来。这是通过压缩堆来完成的。完整收集完成后,Gen1 的新“边界”设置为那些幸存对象之后的空间。

因此,如果您出去尝试清除 Gen0 和/或 Gen1,您会在堆中打开孔,这些孔必须通过压缩“完整”堆(甚至 Gen0 中的对象)来关闭。显然这没有任何意义,因为这些对象中的大多数无论如何都是垃圾。移动它们是没有意义的。并且在(否则压缩)堆上创建和留下大洞是没有意义的。

【讨论】:

  • 从概念上讲,最简单的方法是将 gen2 视为堆的底部,gen1 立即位于顶部,而 gen0 位于最顶部。较旧的对象总是在较年轻的对象之下。发生压缩时,系统会遍历要压缩的代中的所有活动对象,从最旧的对象开始,并将每个对象移动到最低的可用位置。 “gen2 顶部”标记设置在最顶部新复制的 gen1 对象(现在在 gen2 中)的上方。同样,“gen1 顶部”标记设置在新复制的最顶部 gen0 对象的正上方。
  • 所有这些的目标是释放 gen0 之上的连续可用空间。压缩 gen2 很大一部分原因是为了让 gen1 对象向下移动;同样压缩 gen1 允许 gen0 对象向下移动。 .net 使用了一些奇怪的技巧来加快确定对象是否“活”的速度;实际上,它们意味着如果 gen2 对象持有对 gen0 对象的引用,则 gen0 对象将无法收集,直到引用被销毁或 gen2 对象本身可以被收集。
【解决方案4】:

每当系统执行特定代的垃圾收集时,它必须检查每个可能持有对该代任何对象的引用的单个对象。在很多情况下,旧对象只会持有对其他旧对象的引用;如果系统正在执行 Gen0 收集,它可以忽略任何仅包含对 Gen1 和/或 Gen2 的引用的对象。同样,如果它正在执行 Gen1 集合,它可以忽略任何仅包含对 Gen2 的引用的对象。由于对对象的检查和标记占垃圾回收所需时间的很大一部分,因此能够完全跳过旧对象代表了可观的时间节省。

顺便说一句,如果您想知道系统如何“知道”一个对象是否可能持有对较新对象的引用,那么系统有特殊的代码可以在每个对象的描述符中设置几个位(如果对象已写入)。第一位在每次垃圾回收时重置,如果它在下一次垃圾回收时仍被重置,系统将知道它不能包含对 Gen0 对象的任何引用(因为任何对象在上次写入对象时存在并且不存在上一次收集清除的将是 Gen1 或 Gen2)。第二位在每次 Gen1 垃圾回收时重置,如果在下一次 Gen1 垃圾回收时仍重置,系统将知道它不能包含对 Gen0 或 Gen1 对象的任何引用(它持有的任何对象现在都是 Gen2) .请注意,系统不知道也不关心写入对象的信息是否包含 Gen0 或 Gen1 引用。写入未标记对象时所需的陷阱非常昂贵,如果每次写入对象时都必须处理它,则会极大地影响性能。为了避免这种情况,只要发生 任何 写入,对象就会被标记,以便在下一次垃圾收集之前的任何其他写入都可以继续进行而不会中断。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-20
    • 2011-12-22
    • 2023-04-07
    • 2015-08-10
    • 2010-09-30
    • 1970-01-01
    • 1970-01-01
    • 2010-10-31
    相关资源
    最近更新 更多