未分配给变量的类实例是否会过早收集垃圾?
(我甚至不知道我的问题是否有意义;这只是我不理解的东西,并且在我的脑海里旋转了一段时间)
考虑使用以下课程:
public class MyClass { private int _myVar; public void DoSomething() { // ...Do something... _myVar = 1; System.Console.WriteLine("Inside"); } }
并使用这样的类:
public class Test { public static void Main() { // ...Some code... System.Console.WriteLine("Before"); // No assignment to a variable. new MyClass().DoSomething(); // ...Some other code... System.Console.WriteLine("After"); } }
( Ideone )
上面,我正在创建一个类的实例而不将其分配给变量。
我担心垃圾收集器可能会过早删除我的实例。
我对垃圾收集的天真理解是:
“只要没有引用指向它就删除一个对象。”
由于我创建了我的实例而没有将其赋值给变量,因此这种情况是正确的。 显然代码运行正确,所以我的假设似乎是错误的。
有人能告诉我我失踪的信息吗?
总结一下,我的问题是:
(为什么/为什么不这样做)是否可以安全地实例化一个类而不将其作为一个变量或return
它?
即
new MyClass().DoSomething();
和
var c = new MyClass(); c.DoSomething();
从垃圾收集的角度来看是一样的吗?
这有点安全。 或者更确切地说,它就像你有一个在方法调用之后没有使用的变量一样安全。
当GC可以certificate任何东西不再使用任何数据时,一个对象有资格进行垃圾收集(这与说它将立即被垃圾收集不同)。
即使在执行实例方法时,如果该方法不会使用当前执行点以后的任何字段,也会发生这种情况 。 这可能是相当令人惊讶的,但通常不是问题,除非你有一个终结器,这些日子很少见。
当你使用调试器时,垃圾收集器对它将收集的内容更加保守。
这是“早期收集”的演示 – 在这种情况下,早期完成,因为它更容易演示,但我认为它certificate了这一点:
using System; using System.Threading; class EarlyFinalizationDemo { int x = Environment.TickCount; ~EarlyFinalizationDemo() { Test.Log("Finalizer called"); } public void SomeMethod() { Test.Log("Entered SomeMethod"); GC.Collect(); GC.WaitForPendingFinalizers(); Thread.Sleep(1000); Test.Log("Collected once"); Test.Log("Value of x: " + x); GC.Collect(); GC.WaitForPendingFinalizers(); Thread.Sleep(1000); Test.Log("Exiting SomeMethod"); } } class Test { static void Main() { var demo = new EarlyFinalizationDemo(); demo.SomeMethod(); Test.Log("SomeMethod finished"); Thread.Sleep(1000); Test.Log("Main finished"); } public static void Log(string message) { // Ensure all log entries are spaced out lock (typeof(Test)) { Console.WriteLine("{0:HH:mm:ss.FFF}: {1}", DateTime.Now, message); Thread.Sleep(50); } } }
输出:
10:09:24.457: Entered SomeMethod 10:09:25.511: Collected once 10:09:25.562: Value of x: 73479281 10:09:25.616: Finalizer called 10:09:26.666: Exiting SomeMethod 10:09:26.717: SomeMethod finished 10:09:27.769: Main finished
注意在打印x
的值之后如何最终确定对象(因为我们需要对象以便检索x
)但是在 SomeMethod
完成之前 。
其他答案都很好,但我想在这里强调几点。
这个问题基本归结为: 什么时候垃圾收集器可以推断出一个给定的对象已经死了? 答案是垃圾收集器有广泛的自由度来使用它选择的任何技术来确定对象何时死亡 ,这种宽广的范围可以带来一些令人惊讶的结果。
那么让我们开始:
我对垃圾收集的天真理解是:“一旦没有引用指向它就删除一个对象。”
这种理解是错误的错误 。 假设我们有
class C { C c; public C() { this.c = this; } }
现在C
每个实例都有一个存储在其自身内的引用。 如果仅在对它们的引用计数为零时回收对象,则永远不会清除循环引用的对象。
正确的理解是:
某些参考文献是“已知根”。 当一个集合发生时, 跟踪已知的根。 也就是说,所有已知的根都是活着的,活着的东西所指的一切也是活着的,过渡性的。 其他一切都已经死了,并且可以进行填海工程。
不收集需要完成的死对象。 相反,它们在终结队列中保持活动,这是一个已知的根 ,直到它们的终结器运行,之后它们被标记为不再需要完成。 未来的collections将第二次将它们识别为死亡,并且它们将被收回。
很多东西都是已知的根源。 例如,静态字段都是已知的根。 局部变量可能是已知的根,但正如我们将在下面看到的,它们可以以令人惊讶的方式进行优化。 临时值可能是已知的根。
我正在创建一个类的实例而不将其分配给变量。
这里你的问题是一个很好的问题,但它基于一个不正确的假设,即局部变量总是一个已知的根 。 分配对局部变量的引用不一定使对象保持活动状态 。 垃圾收集器可以随意优化局部变量。
我们举个例子:
void M() { var resource = OpenAFile(); int handle = resource.GetHandle(); UnmanagedCode.MessWithFile(handle); }
假设resource
是具有终结器的类的实例,并且终结器关闭该文件。 终结器可以在MessWithFile
之前MessWithFile
吗? 是! resource
是具有整个M
体的生命周期的局部变量的事实是无关紧要的。 运行时可以意识到此代码可以优化为:
void M() { int handle; { var resource = OpenAFile(); handle = resource.GetHandle(); } UnmanagedCode.MessWithFile(handle); }
现在resource
在调用MessWithFile
已经死了。 终结器在GetHandle
和MessWithFile
之间运行是不太可能但合法的,现在我们正在弄乱已经关闭的文件。
这里正确的解决方案是在调用MessWithFile
之后在资源上使用MessWithFile
。
要回到你的问题,你的担心基本上是“是一个已知根的参考的临时位置?” 并且答案通常是肯定的 ,同样需要注意的是,如果运行时可以确定引用永远不会被解引用 ,则允许它告诉GC引用的对象可能已经死亡。
换句话说:你问是否
new MyClass().DoSomething();
和
var c = new MyClass(); c.DoSomething();
从GC的角度来看是相同的。 是。 在这两种情况下, 无论局部变量c
的生命周期如何 ,GC都可以在它确定可以安全地执行此操作时终止对象。
您问题的答案较短: 信任垃圾收集器 。 它经过精心编写,可以做正确的事情。 你唯一需要担心GC做错事的情况就像我提出的那样,终结器的时间对于非托管代码调用的正确性很重要。
当然,GC对您来说是透明的,不会发生早期收集。 所以我想你想知道实现细节:
实例方法的实现类似于带有附加this
参数的静态方法。 在您的情况下, this
值存在于寄存器中,并像这样传递到DoSomething
。 GC知道哪些寄存器包含实时引用,并将它们视为根。
只要DoSomething
可能仍然使用this
值,它就会保持活动状态。 如果DoSomething
从不使用实例状态,那么实际上可以在方法调用仍在其上运行时收集实例。 这是不可观察的,因此是安全的。
只要你谈论单线程环境,你就是安全的。 如果你在DoSomething
方法中开始一个新的线程,那么有趣的事情才会开始发生,如果你的类有一个终结器,就会发生更多的乐趣。 这里要理解的关键是你和运行时/优化器/等之间的许多契约只在一个线程中有效。 当您开始使用一种非主要面向multithreading的语言在多个线程上进行编程时,这是带来灾难性后果的事情之一(是的,C#是其中一种语言)。
在你的情况下,你甚至使用this
实例,这使得意外收集更不可能在仍然在该方法内; 在任何情况下,合同都是在单个线程上,您无法观察优化和未优化代码之间的差异(除了内存使用,速度等,但那些是“免费午餐”)。