C# .NET对象释放

8

这应该很简单。假设我有以下代码:

void Method()
{
   AnotherMethod(new MyClass());
}

void AnotherMethod(MyClass obj)
{
   Console.WriteLine(obj.ToString());
}

如果我调用“Method()”,在该过程中创建的MyClass对象会发生什么? 即使没有任何东西使用它,它是否仍然存在于堆栈中? 还是会立即被删除?
我是否必须将其设置为null以更快地使GC注意到它?

与C++不同,像Java/C#这样的语言中,对象不会存在于堆栈上;只有对对象的引用存在于堆栈上,对象本身存储在堆上。 - BlueRaja - Danny Pflughoeft
7个回答

13

在调用 Method 方法完成后,您的 MyClass 对象仍然存在于内存中,但没有根值引用它。因此,它会一直存在,直到下一次 GC 运行时收集它并回收内存。

实际上,您无法加速此过程,除非强制执行GC。但是这可能是一个坏主意。 GC 旨在清理这些对象,而您对其加速的任何尝试很可能导致整体变得更慢。您还会发现,虽然正确地清理托管对象,但 GC 可能实际上不会减少系统中的内存。这是因为 GC 将其保留以备将来使用。这是一个非常复杂的系统,通常最好让其自己操作。


1
不要看幕后的人! :) 我真的很喜欢把所有这些交给GC处理。少了一件事要担心。 - Tad Donaghe

7
如果我调用“Method()”,那么在此过程中创建的MyClass对象会发生什么?
它会被创建在GC堆上。然后在栈上放置一个指向其在堆中位置的引用。然后发生对AnotherMethod的调用。接着调用对象的ToString方法并将结果打印出来。然后AnotherMethod返回。
在调用之后,即使没有任何东西使用它,它是否仍存在于堆栈中?
你的问题含糊不清。通过“调用”您是否是指对Method的调用还是AnotherMethod的调用?这很重要,因为此时,无论是否编译了开启优化,堆内存是否可以进行垃圾回收都取决于。我将稍微更改您的程序以说明差异。假设您有:
void Method() 
{ 
   AnotherMethod(new MyClass()); 
   Console.WriteLine("Hello world");
} 

关闭优化后,我们有时会生成类似于以下代码的代码:

void Method() 
{ 
   var temp = new MyClass();
   AnotherMethod(temp); 
   Console.WriteLine("Hello world");
} 

在未优化的版本中,运行时实际上会选择在 Method 返回后,在 WriteLine 之后将对象视为不可收集。 在优化版本中,运行时可以选择在 AnotherMethod 返回之前将对象视为可收集,而不是在 WriteLine 之后。
这种差异的原因是在调试会话期间使对象生命周期更加可预测通常有助于人们理解他们的程序。
“它会立即被删除吗?”
没有什么会立即被回收;垃圾回收器在感觉需要运行时才会运行。如果您需要一些资源(如文件句柄)在使用完毕后立即清理,请使用“using”块。如果不需要,请让垃圾回收器决定何时回收内存。
“我必须将其设置为 null 才能让 GC 更快地注意到它吗?”
你必须将什么设置为 null?你想到了哪个变量?
无论如何,您不必做任何事情来使垃圾回收器工作。它可以自己良好地运行,不需要您提示。
我认为您正在过度思考这个问题。让垃圾回收器完成它的工作,不要过度关注它。如果您在内存没有及时回收方面遇到了真正的实际问题,请向我们展示一些说明该问题的代码;否则,请放松并学会爱上自动存储回收。

太好了!当我在你的第二个示例中(关闭优化)说“设置为空”时,这与我在AnotherMethod中使用后将“temp = null”相同。之所以提出这个问题是因为我在我的代码中遇到了无法序列化的异常。假设Method()存在于“TopClass”中。 我已将“TopClass”标记为可序列化。 MyClass无法序列化。 我想GC没有足够快地工作,而不可序列化的对象(MyClass)留在了可序列化的对象中。 尝试序列化“TopClass”将引发异常。 - Nick
2
我认为你的假设不太可能真正解释序列化问题。也许你的对象中有一个字段包含对非可序列化对象的引用? - Eric Lippert
同意 Eric Lippert 关于序列化的观点(我又有什么好反对的呢)。序列化处理的是类型的数据而非代码。方法内部的任何“局部”内容都不应该影响序列化。 - Joel Coehoorn
1
@Nick - 在许多情况下,BinaryFormatter 的意外序列化问题往往是由于事件订阅引起的,底层委托字段未标记为 [NonSerialized]。您还可以使用字段式事件来实现此操作,如 [field:NonSerialized] public event EventHandler MyEvent; - Marc Gravell
现在如果我们能够自动实现属性,那该多好啊![低头...] - Marc Gravell

3

实际上,该实例将在堆上声明,但请看Eric Lipper的文章The Stack is an Implementation Detail

无论如何,因为函数执行后不会再有对该实例的引用,所以它可能会在未来某个未定义的时间点被垃圾收集器删除。关于确切的时间点,这是未定义的,但您基本上也不需要担心;GC有复杂的算法来帮助确定何时进行收集。


3
重点在于语义。您问的是方法调用后它是否仍然存在于栈中。答案是否定的,它已经从栈中移除了。但这并不是最终的情况。对象确实仍然存在,只是不再有根节点。它不会被销毁或回收,直到GC运行时。但在这个时候,它不再是你的问题了。GC比你或我更擅长决定何时收集某些东西。
几乎没有好的理由这样做。唯一能帮助的情况是如果您有一个非常长时间运行的方法,以及一个您早已完成的对象,否则该对象将一直存在于该方法的作用域之内,直到该方法结束。即便如此,在GC在方法执行期间进行决策的罕见情况下,将其设置为null也仅仅是有用的。但在这种情况下,你可能还会犯其他错误。

2
实际上,CLR GC 可以在方法运行时清理非空变量,如果它能确定该变量在方法的其余部分不会被使用。这就是为什么GC.KeepAlive()必须存在的原因(尽管除非你真的非常需要,否则永远不应该使用该方法)。因此,将变量设置为null不应该有任何影响。 - Daniel Pryden

2
在C#中,new MyClass()的作用域仅限于在Method()函数执行期间。一旦AnotherMethod()执行完成,它就会超出范围并被解除引用。然后它仍然留存在堆上,直到GC运行其收集循环并将其识别为未引用的内存块。因此,它仍然存在于堆上,但无法访问。

1

垃圾回收器会跟踪哪些对象在代码中可能仍然被引用。然后,它会定期检查是否有任何仍然存活的对象不可能在代码中被引用,并清理它们。

这个机制有些复杂,垃圾回收的时间取决于各种因素。垃圾回收器旨在在最优化的时间(它可以确定的时间)进行这些回收,因此,虽然可以强制进行回收,但几乎总是一个坏主意。

将变量设置为null对对象处理的速度几乎没有影响。虽然在某些小的角落情况下可能会有所帮助,但这并不值得在代码中添加无用的赋值语句,这只会损害可读性而不会影响代码性能。

垃圾回收器旨在尽可能有效,而无需您考虑太多。老实说,您真正需要注意的唯一事情就是在分配长时间保持活动状态的大型对象时要小心,但在我的经验中,这种情况通常很少出现。


-4
据我所知,该对象仅在您的方法上下文中有效。在执行“Method()”方法后,它将被添加到处理队列中。

你所说的“dispose queue”是什么?我从未听说过。你能解释一下你的意思吗? - Eric Lippert
我的意思是该对象被标记为“可供处理”。我已经阅读过这些对象被添加到队列中由GC进行处理。 - llullulluis
我认为你在考虑待处理的终结器。严格来说,你的陈述是正确的;对象引用仅在方法终止之前逻辑上有效。在方法完成后的某个未指定时间,垃圾回收器最终会发现该对象不再具有根性,将其标记为终结器,对其进行终结并将内存返回给GC堆。但是,将GC描述为在控制离开每个方法时积极排队死亡对象是不正确的;这根本不是它的工作方式。 - Eric Lippert
好的,对不起我犯了个错。 - llullulluis

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