对 .NET 垃圾收集器的质疑

Posted

技术标签:

【中文标题】对 .NET 垃圾收集器的质疑【英文标题】:Doubts about .NET Garbage Collector 【发布时间】:2010-04-08 11:32:10 【问题描述】:

我已经阅读了一些关于 .NET 垃圾收集器的文档,但我仍然有一些疑问(C# 中的示例):

1) GC.Collect() 是调用部分集合还是完整集合? 2) 部分收集是否会阻止“受害者”应用程序的执行?如果是的话..那么我想这是一件非常“轻松”的事情,因为我正在运行一个使用 2-3GB 内存的游戏服务器,并且我“从不”执行停止(或者我看不到它们.. )。 3)我读过关于 GC 根的信息,但仍然无法理解它们是如何工作的。假设这是代码(C#):

我的班级1:

[...] 
public List<MyClass2> classList = new List<MyClass2>(); 
[...]

主要:

main()
    
     MyClass1 a = new MyClass1();
     MyClass2 b = new MyClass2();
     a.classList.Add(b);

     b = null;

     DoSomeLongWork();
    

b 是否有资格被垃圾收集(在 DoSomeLongWork 完成之前)? classList 包含的对 b 的引用,可以认为是根吗? 或者根只是对实例的第一个引用? (我的意思是,b 是根引用,因为实例化发生在那里)。

【问题讨论】:

【参考方案1】:

    GC.Collect 有几个重载。没有任何参数的那个会做一个完整的收集。但请记住,显式调用 GC 很少是一个好主意。

    垃圾收集发生在托管应用程序中。 GC 可能必须暂停该进程中的所有托管线程才能压缩堆。如果这是您所要求的,它不会影响系统上的其他进程。对于大多数应用程序,这通常不是问题,但由于频繁收集,您可能会遇到与性能相关的问题。有几个相关的性能计数器(GC 时间百分比等)可用于监控 GC 的执行情况。

    将根视为有效引用。在您的示例中,如果仍有对它的引用,则不会收集 MyClass2 的实例。 IE。如果 a 指向的实例仍然是根的,那么您的 MyClass2 实例也会。另外,请记住,GC 在发布模式构建中比在调试构建中更具侵略性。

【讨论】:

【参考方案2】:

    GC.Collect() 进行完整的收集。这就是为什么自己调用它不是一个好主意,因为它会过早地将对象提升一代

    AFAIK,不是真的。 GC 阻塞的时间太短以至于无关紧要。

    堆栈 GC 根是当前被堆栈上的方法引用的对象。请注意,调试版本和发布版本之间的行为是不同的;在调试构建时,方法中的所有变量都保持活动状态,直到方法返回(因此您可以在调试窗口中看到它们的值)。在发布构建时,CLR 会跟踪使用方法变量的位置,并且 GC 可以删除当前正在执行的方法引用但未在仍要执行的方法部分中使用的对象。

    李>

因此,在您的示例中,ab 在调用 DoSomeLongWork() 之后都不会再次被引用,因此在发布版本中,在该方法执行期间,它们都有资格被收集。在调试版本中,它们会一直徘徊,直到 main() 方法返回。

【讨论】:

关于您的第三个答案,您为什么说“如果 b = null; 行不存在”?如果行 WAS 那里你描述的事情(在发布模式下)不会发生?而且我仍然无法理解 List 中的引用是否可以作为有效引用,或者如果我执行 b = null 然后垃圾收集器运行,我会发现 List 引用指向 null。 抱歉,我编辑了我的帖子。列表中的引用不计为根,因为您只能通过 a 访问它 - 并且该对象符合收集条件,因此列表符合条件(如果没有其他引用它),因此 b 符合条件. a 和 b 不再被引用之前 DoSomeLongWork。如果在 DoSomeLongWork 运行时发生 GC,它们都会被垃圾回收。这不会导致问题,因为没有参考。 我想我在第三个问题中写了一个部分“错误”的例子。假设 b = null 出现在 DoSomeLongWork() 中,a 和 b 是某个类的属性,并且在该方法中有引用 a 和 b 的代码,如果我希望 GC 释放 b 内存,那么 b = null 就足够了,或者,正如有人在其他答案中所说,我还必须删除列表中的引用? 我还请教一个建议(希望我不会太离题):当我们的游戏服务器使用超过 3.6GB 时,我们会为我们的游戏服务器调用 GC.Collect(),因为我们担心每个 4GB应用程序限制,因为我们想知道何时发生完整收集(我们发送游戏内消息)。它是否正确?还有另一种更好的方法吗?最后,每个应用程序的 4GB 限制是真的还是每个 .Net 对象? (如使用 4GB 的列表)。如果是真的有办法使用超过 4GB 的内存吗?抱歉所有这些问题,但它们都是相关联的,如果我要跑题,我会在其他地方找到答案【参考方案3】:

垃圾回收是automatic。除非您正在处理非托管资源,否则您不必干预。每当对象超出范围时,就会收集垃圾;并且在垃圾收集器认为有必要的特定时间间隔 - 例如,操作系统需要内存。这意味着无法保证会在多长时间内完成,但在这些对象的内存被回收之前,您的应用程序不会耗尽内存。

b 是否有资格被垃圾收集(在 DoSomeLongWork 完成之前)?

是的,只要垃圾收集器认为有必要。

结帐Garbage Collector Basics and Performance Hints

【讨论】:

【参考方案4】:

    是的,GC.Collect() 会执行完整的收集,但您可以手动执行 GC.Collect(0) 来仅执行第 0 代。

    所有收集都会阻塞线程,但部分收集只会非常短暂地阻塞线程。

    不,MyClass2 的实例在另一个列表中仍然可以访问。 ab 都是根引用(在 DoSomeLongWork 期间),但由于 b 为空,这并不重要。请注意,GC 与引用类型的概念非常相关。 b 只是一个引用匿名对象的本地变量。 GC 的“根”是静态字段、堆栈上的所有内容,甚至是 CPU 寄存器。以更简单的方式:您的代码仍然可以访问的所有内容。

【讨论】:

AFAIK,3 不正确。 CLR 跟踪变量在方法中的引用位置,并在此之前进行垃圾收集 @thecoop:不,JIT 优化器可以提前终止 b 引用,但由于实例位于另一个列表中,因此在这里没有区别。但它通常会这样做。 @thecoop:我明白了,a 有可能很早就被优化掉了,但这不是问题的意图。 同意thecoop - a 和 b 不是根,它们是 main 函数的本地函数,在 DoSomelongWork() 开始后都不会被引用并且可以被收集。 请阅读我的 cmets 其他答案。假设 a 和 b 不仅是变量,而且是另一个类的字段,并且 a 不是垃圾收集的。【参考方案5】:

b 是否有资格被垃圾收集(在 DoSomeLongWork 完成之前)?

可能是的,前提是 ab 被编译器从全局根集合中排除(即不保留在堆栈中),那么它们可以在 DoSomeLongWork 期间由集合回收。

我发现 .NET 可以愉快地回收但 Mono leaks memory。

classList包含的b的引用,能不能算是根?

b 是否会变成全局根完全取决于编译器。

玩具编译器可能会将来自函数参数和从函数调用返回的引用推送到堆栈上,并在每个函数结束时展开堆栈。在这种情况下,b 将被压入堆栈,因此将成为全局根。

生产质量编译器执行复杂的寄存器分配并仅在堆栈上维护活动引用,在堆栈上的引用死亡时覆盖或清空引用。在这种情况下,b 在对 DoSomeLongWork 的调用过程中已失效,因此其堆栈条目将被清空或覆盖。

如果不详细说明编译器将做什么,就无法从源代码中推断出所有这些。例如,我的HLVM项目现阶段只是一个玩具,使用前一种技术,但在这种情况下它实际上会收集ab,因为对DoSomeLongWork的调用是尾调用。

或者根只是对实例的第一个引用?

全局根是 GC 开始的引用,以便遍历堆中所有可到达的数据。全局根通常是全局变量和线程堆栈,但更复杂的 GC 算法可以引入新类型的全局根,例如记住的集合。

【讨论】:

以上是关于对 .NET 垃圾收集器的质疑的主要内容,如果未能解决你的问题,请参考以下文章

.net 垃圾收集和高 CPU

强制垃圾收集对活动对象以及长寿命对象有什么影响?

垃圾收集器 .Net 将收集 Stack() [关闭]

垃圾回收机制

面试官:谈谈你对G1垃圾收集器都有哪些了解?

.Net Discovery系列之三 深入理解.Net垃圾收集机制(上)