C# 动作和垃圾回收机制

3

我正在使用C#中的Actions,想知道当我希望GC正确清理对象时,是否需要将Action实例设置为null?以下是示例:

public class A
{
 public Action a;
}

public class B
{
  public string str;
}

public class C
{
 public void DoSomething()
 {
   A aClass = new A();
   B bClass = new B();
   aClass.a = () => { bClass.str = "Hello"; }
 }
}

在我的Main方法中,我有类似这样的代码:

public void Main(...)
{
  C cClass = new C();
  cClass.DoSomething();

  Console.WriteLine("At this point I dont need object A or B anymore so I would like the GC to collect them automatically.");
  Console.WriteLine("Therefore I am giving GC time by letting my app sleep");
  Thread.Sleep(3000000);
  Console.WriteLine("The app was propably sleeping long enough for GC to have tried collecting objects at least once but I am not sure if A and B objects have really been collected");
 }
}

请阅读Console.WriteLine文本,它将帮助您理解我在这里提出的问题。
如果我将我的GC理解应用于此示例,则GC永远不会收集对象,因为A不能被销毁,因为它持有B的实例。我是正确的吗?
如何正确地收集这两个对象?我需要将Actions的实例设置为null,以便让GC在应用程序结束之前收集对象,还是已经存在某种非常智能的机制可以销毁具有类似A和B的Actions的对象?
编辑:问题是关于GC和正确收集对象。它不是关于调用方法collect()。

2
你应该先调用 GC.Collect(),然后再调用 GC.WaitForPendingFinalizers() 来确保 GC 已经运行。长时间的睡眠并不能保证 GC 会运行。 - dlev
1
调用 Thread.Sleep 不会触发 GC - 它通常只在无法在第 0 代中进行分配时运行。您可以使用 GC.Collect() 强制运行它。 - Lee
1
在调试模式下,GC不会收集当前方法引用的对象,因此请确保在发布模式下运行。 - tukaef
1
@Aron 我并没有回答问题的任何部分。我只是指出,如果 OP 想要确保 GC 运行,那么他需要确保 GC 运行。此外,在这里存在闭包实际上并不相关。 - dlev
1
@dlev 这个问题涉及到GC在处理捕获时的行为,即它是否会收集捕获。 - Aron
显示剩余6条评论
2个回答

19

这个问题存在很多问题。我不会直接回答你的问题,而是会回答你应该问什么问题。

首先让我们打消您对垃圾收集器的观念。

  

长时间睡眠会激活垃圾收集器吗?

不会。

  

什么会激活垃圾收集器?

为了测试目的,您可以使用 GC.Collect()GC.WaitForPendingFinalizers()。 仅在测试目的中使用它们; 除非在某些极其罕见的情况下,在生产代码中使用它们是一种不好的实践。

在正常情况下,触发 GC 的事情很复杂; GC 是一个经过高度调整的机器。

  

就闭合的外变量而言,垃圾收集的语义是什么?

将转换为委托的 lambda 的闭合的外部变量的 生命周期 扩展为 不短于委托的生命周期

  

假设我有一个类型为 Action 的变量,该变量由引用类型的外部局部变量闭合的 lambda 初始化。为了使该变量引用的对象可以被收集,我是否需要将类型为 Action 的变量设置为 null

在绝大多数情况下,不需要。垃圾收集器非常聪明; 让它自己完成工作,不要担心它。最终,运行时将确定 Action 变量不能被任何活动根访问,并使其符合收集资格; 然后闭合的外部变量将变得合适。

可能存在极其罕见的情况,您希望更早地丢弃对 Action 的引用,但这些情况很少; 在大多数时间里,让 GC 不受干扰地完成其工作即可。

  

是否存在外部变量生命周期过长的情况?

是的。看一下:

void M()
{
    Expensive e = new Expensive();
    Cheap c = new Cheap();
    Q.longLived = ()=>c; // static field
    Q.shortLived = ()=>e; // static field
}

执行M()时,将为两个委托创建闭包。假设shortLived很快就会设置为null,而longLived则在遥远的未来设置为null。不幸的是,即使只有c仍然可达,两个局部变量的生命周期也被延长到longLived引用的对象的生命周期。昂贵的资源e直到longLived中的引用死亡才会被释放。

许多编程语言都存在这个问题; JavaScript、Visual Basic 和 C# 的某些实现都有这个问题。有些人谈论在 C# / VB 的 Roslyn 发行版中修复它,但我不知道是否会实现。

在这种情况下,解决方案是在第一时间避免这种情况;如果其中一个委托的生存期比另一个委托更长,请勿创建共享闭包的两个 lambda。

何时会出现一个不是闭合外部变量的本地变量变得可以被收集的情况?

当运行时能够证明一个局部变量无法再次读取时,它引用的内容就可以被收集(当然,假设该局部变量是唯一的根。)在您的示例程序中,aClassbClass中的引用不需要保持活动状态直到方法结束。事实上,在一个线程上正在为对象分配内存时,另一个线程可能会在该对象的构造函数中仍然使用它! GC 可以非常积极地确定什么是死的,所以要小心。

如何在面对积极的 GC 时保持某些东西的存活?

当然是使用:GC.KeepAlive()


6
我正在使用C#中的Actions,想知道当我希望GC正确地回收对象时,是否需要将Action的实例设置为null?
不一定。只要没有引用委托的任何对象可达,委托就有资格进行垃圾回收。
话虽如此,在你的例子中,aClass和bClass仍然是有效变量,并引用可达对象。这意味着aClass.a仍然是可达的,不符合垃圾回收的条件,因此它不会被收集。
如果您希望对其进行垃圾回收,则需要显式将对象引用(aClass)设置为null,以使A实例及其包含的委托不再是可达对象,然后您必须显式调用GC.Collect来触发GC,因为在您的代码中没有任何东西会导致GC触发。

@snowyhedgehog aClass 仍然在相同的时间超出作用域,就像 bClass 一样。但是,如果您要返回 aClass,那将是另一回事...这也是我猜想您想了解的内容。 - Aron
3
这些评论中的错误信息水平非常高。@snowyhedgehog:你应该只相信这些评论中一半左右的内容,因为大约只有这么多是正确的。 - Eric Lippert
1
@Aron:关于bClass只有在aClass完全为false之后才能收集的问题。 - Eric Lippert
3
关于你的程序中必须按照什么顺序发生的所有假设都是错误的。我们可以讨论在特定实现中会发生什么,但那只是一个实现细节,并非要求。 - Eric Lippert
4
在30秒内,垃圾回收器的工作原理如下:想象一面满是红色方框并有箭头相连的白板。选出一些方框作为“存活”并将其涂成绿色。现在,将每个被“存活”方框所指向的红色方框也涂成绿色。继续这样做,直到没有更多被“存活”方框所指向的红色方框为止。然后,擦掉白板上的所有红色方框,并重新将所有绿色方框涂成红色。这就是垃圾回收器的工作原理。当JIT编译器知道aClass不再被使用时,它停止将其标记为“存活”,保持为红色,并消失。 - Eric Lippert
显示剩余33条评论

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