C# 未分配给变量的类实例是否会过早地被垃圾收集?
(我甚至不知道我的问题是否有意义;它只是我不理解的东西,在我的脑海里转了一段时间) 考虑参加以下课程:C# 未分配给变量的类实例是否会过早地被垃圾收集?,c#,.net,garbage-collection,C#,.net,Garbage Collection,(我甚至不知道我的问题是否有意义;它只是我不理解的东西,在我的脑海里转了一段时间) 考虑参加以下课程: public class MyClass { private int _myVar; public void DoSomething() { // ...Do something... _myVar = 1; System.Console.WriteLine("Inside"); } } 像这样使用这个类:
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");
}
}
()
在上面,我创建了一个类的实例,而没有将其分配给变量
我担心垃圾收集器会过早删除我的实例
我对垃圾收集的天真理解是:
“一旦没有引用指向某个对象,就立即将其删除。”
由于我创建实例时没有将其分配给变量,因此此条件为真。显然代码运行正确,所以我的假设似乎是错误的
有人能告诉我我丢失的信息吗
总而言之,我的问题是:
(为什么/为什么不)实例化一个类而不将其赋给变量或返回它是否安全
即是
new MyClass().DoSomething();
new MyClass().DoSomething();
及
从垃圾收集的角度来看也是一样的?当然,GC对您来说是透明的,不会发生早期收集。因此,我猜您想了解实施细节:
实例方法的实现类似于静态方法,并带有一个附加的此
参数。在您的例子中,这个
值存在于寄存器中,并像那样被传递到DoSomething
。GC知道哪些寄存器包含活动引用,并将它们视为根
只要DoSomething
可能仍然使用此
值,它就会保持活动状态。如果DoSomething
从不使用实例状态,那么确实可以在实例上仍在运行方法调用时收集该实例。这是不可观察的,因此是安全的。这有点安全。或者更确切地说,它就像您有一个变量一样安全,该变量无论如何在方法调用之后都不会被使用
当GC可以证明不再有任何东西要使用它的任何数据时,对象有资格进行垃圾收集(这与说它将立即被垃圾收集不同)
如果实例方法从当前执行点开始不使用任何字段,则即使在实例方法执行时也可能发生这种情况。这可能会非常令人惊讶,但除非您有一个终结器,否则通常不会出现问题,这在当今是非常罕见的
当您使用调试器时,顺便说一句,垃圾收集器在收集内容方面要保守得多
这是一个“早期收藏”的演示——在这种情况下,早期定稿更容易演示,但我认为它足够清楚地证明了这一点:
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
完成之前,对象是如何完成的。只要您谈论的是单线程环境,您就安全了。有趣的事情只有在DoSomething
方法中启动一个新线程时才会发生,如果类有一个终结器,则会发生更有趣的事情。这里要理解的关键是,您和运行时/优化器/等之间的许多契约仅在单个线程中有效。当您开始使用一种不主要面向多线程的语言(是的,C#就是其中之一)在多线程上编程时,这是造成灾难性后果的原因之一
在您的例子中,您甚至使用了this
实例,这使得仍然在该方法中的意外收集的可能性更小;在任何情况下,契约是,在单个线程上,您无法观察优化和未优化代码之间的差异(除了内存使用、速度等,但这些都是“免费午餐”)。其他答案都很好,但我想在这里强调几点
问题本质上归结为:何时允许垃圾收集器推断给定对象已死亡?答案是,垃圾收集器有很大的自由度来使用它选择的任何技术来确定对象何时死亡,而这种广阔的自由度可能会导致一些令人惊讶的结果
因此,让我们从以下方面开始:
我对垃圾收集的天真理解是:“一旦没有引用指向对象,就立即删除它。”
这种理解是错误的。假设我们有
class C { C c; public C() { this.c = this; } }
现在,C
的每个实例都在其内部存储了对它的引用。如果对象仅在引用计数为零时被回收,则循环引用的对象将永远不会被清理
正确认识是:
某些引用是“已知根”。当集合发生时,将跟踪已知的根。也就是说,所有已知的根都是活着的,而活着的事物所指的一切也都是活着的,传递的。其他一切都死了,可以重新利用
不收集需要终结的死对象。相反,它们在终结队列(已知根)上保持活动状态,直到它们的终结器运行,之后它们被标记为不再需要终结。未来的收集将再次确认它们已死亡,并将回收它们
很多东西都是已知的根。例如,静态字段都是已知的根。局部变量可能是已知的根,但正如我们将在下面看到的,它们可以以令人惊讶的方式进行优化。临时值可能是已知的根
我正在创建一个类的实例,但没有将其分配给变量
你的问题很好,但它基于一个错误的假设,即局部变量总是一个已知的根将引用指定给局部变量并不一定使对象保持活动状态。垃圾收集器可以随心所欲地优化局部变量
void M()
{
var resource = OpenAFile();
int handle = resource.GetHandle();
UnmanagedCode.MessWithFile(handle);
}
void M()
{
int handle;
{
var resource = OpenAFile();
handle = resource.GetHandle();
}
UnmanagedCode.MessWithFile(handle);
}
new MyClass().DoSomething();
var c = new MyClass();
c.DoSomething();