只在第二代和大对象堆上执行GC.Collect

10

我的应用程序有一个特定的时间点,当许多大对象同时被释放时。那个时候,我想专门在大对象堆(LOH)上进行垃圾回收。

我知道你无法这样做,必须调用GC.Collect(2),因为仅当它执行第二代垃圾回收时,GC才会对LOH进行回收。然而,根据文档中的说明,调用GC.Collect(2)仍会在第一代和第零代上运行GC。

是否可以强制GC 收集第二代,而不包括第一代或第零代?

如果不可能,那么GC设计的原因是什么?


为什么你想要这样做,即不从第0代或第1代进行收集?当让.NET GC自行运行时,它的表现最佳。 - thecoop
我知道这点。基本上,你永远不想手动强制进行GC,因为它们是密集型操作。既然如此,当我看到需要运行GC的情况时,我希望它只针对特定代运行,而不是执行完整的GC。我正在尝试更加注意我的GC使用方式,但它并没有让我更具选择性。 - DevinB
4个回答

14

不可能。垃圾回收器是这样设计的:第二代垃圾回收会同时回收第一代和第零代。

编辑:一位垃圾回收开发人员的博客中找到了一个相关的源:

第二代垃圾回收需要进行全面的回收(包括第零代、第一代、第二代和大对象!每次第二代垃圾回收都会对大对象进行回收,即使回收并非由大对象空间不足引起。请注意,并不存在只回收大对象的垃圾回收方式。)比年轻一代的回收时间要长得多。

编辑2:根据同一篇博客的第1部分第2部分,显然第零代和第一代的回收速度比第二代快得多,因此仅进行第二代回收可能并不能带来很多性能优势。可能存在更根本的原因,但我不确定。也许这篇博客上有相关文章提供答案。


谢谢!请注意,我已经编辑了我的问题,也问了为什么它被限制在这种方式。 - DevinB

6
由于除大对象之外的所有新分配总是进入Gen0,因此GC设计为始终从指定的代及以下收集。当您调用GC.Collect(2)时,表示要求GC从Gen0、Gen1和Gen2进行收集。
如果您确定正在处理大量的大对象(在分配时足够大以放置在LOH上),最好的选择是确保在完成后将它们设置为空(在VB中为Nothing)。 LOH分配尝试聪明地重用块。例如,如果您在LOH上分配了一个1MB的对象,然后处理并将其设置为空,您将得到一个1MB的“空洞”。下一次您在LOH上分配任何小于等于1MB的内容时,它将填补该空洞(并持续填补,直到下一次分配的内容太大而无法适应剩余空间时,它将分配一个新块)。
请记住,.NET中的代不是物理事物,而是逻辑上的分离,以帮助增加GC性能。由于所有新分配的内容都进入Gen0,因此这总是第一个要被收集的代。每个运行的收集周期中,任何在较低代中幸存下来的内容都将“晋升”到下一个更高的代(直到达到Gen2)。
在大多数情况下,GC不需要超出收集Gen0。当前GC的实现能够同时收集Gen0和Gen1,但无法在收集Gen0或Gen1时收集Gen2。(.NET 4.0大大放宽了这个限制,在很大程度上,GC能够在收集Gen0或Gen1时也收集Gen2。)

你的解释对GC如何工作提供了很好的概述,但它并没有说明为什么有一个限制条件,防止严格的第一代或第二代收集。 - DevinB
2
设置 myVar = null 没有任何作用。请参见 http://www.bryancook.net/2008/05/net-garbage-collection-behavior-for.html 底部。 - DevinB
我相信原因是将对象提升到更高的代中只有在进行垃圾回收期间才会被评估(并受到影响),因此如果您不收集较低的代,则可能没有什么(新的)工作要做... - Glenn Slayden

0
每当系统对特定代进行垃圾回收时,它必须检查可能持有对该代中任何对象的引用的每个单个对象。在许多情况下,旧对象只会持有对其他旧对象的引用;如果系统正在进行Gen0收集,则可以忽略仅持有对Gen1和/或Gen2对象引用的任何对象。同样,如果正在进行Gen1收集,则可以忽略仅持有对Gen2对象引用的任何对象。由于对象的检查和标记占据了垃圾回收所需时间的很大一部分,因此能够完全跳过较旧的对象代表了相当大的时间节省。
顺便提一下,如果你想知道系统如何“知道”一个对象是否可能持有对新对象的引用,那么系统有特殊的代码来设置每个对象描述符中的几个位,如果对象被写入,则第一位会被重置在每次垃圾回收时,并且如果在下一次垃圾回收时它仍然被重置,则系统将知道它不能包含任何对Gen0对象的引用(因为当对象最后被写入时存在的任何对象,并且未被前一次垃圾回收清除的任何对象将是Gen1或Gen2)。 第二位是在每次Gen1垃圾回收时重置的,如果在下一次Gen1垃圾回收时它仍然被重置,则系统将知道它不能包含任何对Gen0或Gen1对象的引用(现在它持有引用的所有对象都是Gen2)。 请注意,系统不知道也不关心写入对象的信息是否包括Gen0或Gen1引用。写入未标记对象时所需的陷阱很昂贵,如果每次写入对象时都必须处理它,那么将极大地影响性能。为了避免这种情况,只要进行任何写操作,对象就会被标记,以便在下一次垃圾回收之前可以进行任何其他写操作而不会中断。

0

回答“为什么”的问题:从物理上讲,Gen0、Gen1或Gen2并不存在。它们都使用虚拟地址空间上的同一内存块。它们之间的区别只是通过移动想象中的边界限制来虚拟地进行区分。

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

因此,如果您只尝试清除Gen0和/或Gen1,您将在堆中打开空洞,必须通过压缩“完整”堆来关闭这些空洞 - 即使是Gen0中的对象也是如此。显然,这没有任何意义,因为其中大多数对象都是垃圾。没有必要将它们移动。在(否则紧凑的)堆上创建和留下大型空洞也没有意义。


从概念上来说,最简单的想法是将gen2物品视为堆栈底部,紧接着是gen1,最顶端是gen0。较旧的对象总是在较年轻的对象下方。当压缩发生时,系统遍历要压缩的一代中的所有活动对象,从最旧的对象开始,并将每个对象移动到最低可用位置。“gen2的顶部”标记设置在最上面新复制的gen1对象(现在在gen2中)的正上方。同样,“gen1的顶部”标记设置在最上面的新复制的gen0对象的正上方。 - supercat
所有这些的目标是为了释放gen0上方的连续可用空间。压缩gen2的一个重要原因是允许gen1对象向下移动;同样,压缩gen1允许gen0对象向下移动。.net使用一些奇怪的技巧来加速确定对象是否“活动”的过程;实际上,这意味着如果gen2对象持有对gen0对象的引用,则除非销毁引用或者可以收集gen2对象本身,否则gen0对象将无法被收集。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接