你需要处理对象并将它们设置为null,还是垃圾收集器会在它们超出范围时清理它们?
你需要处理对象并将它们设置为null,还是垃圾收集器会在它们超出范围时清理它们?
当对象不再使用时,它们将被清理,垃圾收集器也会在合适的时候进行清理。有时,您可能需要将一个对象设置为null
以使其超出范围(例如您不再需要其值的静态字段),但通常没有必要将其设置为null
。
关于释放对象,我赞同@Andre的观点。如果对象是IDisposable
接口实现类,那么当您不再需要该对象时,最好将其释放,特别是当对象使用非托管资源时。未正确释放非托管资源会导致内存泄漏。
您可以使用using
语句,在程序离开using
语句的范围时自动释放对象。
using (MyIDisposableObject obj = new MyIDisposableObject())
{
// use the object here
} // the object is disposed here
这个与以下函数的功能等效:
MyIDisposableObject obj;
try
{
obj = new MyIDisposableObject();
}
finally
{
if (obj != null)
{
((IDisposable)obj).Dispose();
}
}
IDisposable
,未能 Dispose
对象通常不会导致内存泄漏,因为任何设计良好的类都不会出现这种情况。在使用 C# 处理非托管资源时,应该编写一个终结器用于释放非托管资源。这意味着它不会在应该释放资源的时候进行解除分配,而是推迟到垃圾回收器对托管对象进行终结时才进行。然而,这仍然可能会导致许多其他问题(例如未释放的锁)。但是,您应该始终 Dispose
一个 IDisposable
! - AidiakapiC#中的对象永远不会像C++那样失去作用域,它们在不再使用时由垃圾回收器自动处理。这比C++更复杂,因为变量的作用域完全是确定性的。CLR垃圾收集器主动遍历所有已创建的对象,并判断它们是否正在使用。
一个对象可以在一个函数中“超出作用域”,但如果它的值被返回,则GC将查看调用函数是否持有返回值。
将对象引用设置为null
是不必要的,因为垃圾收集是通过找出哪些对象被其他对象引用来工作的。
实际上,你不必担心销毁,它只是工作得很好,这很棒 :)
当你完成对实现了IDisposable
的所有对象的操作时,必须调用Dispose
。通常,您会像这样使用using
块来处理这些对象:
using (var ms = new MemoryStream()) {
//...
}
编辑:关于变量作用域的问题。Craig问变量作用域是否对对象生命周期有影响。为了正确解释CLR的这个方面,我需要解释一下C++和C#中的一些概念。
在两种语言中,变量只能在定义它的范围内使用 - 类、函数或由大括号括起来的语句块。然而,微妙的区别在于,在C#中,变量不能在嵌套块中重新定义。
在C++中,这是完全合法的:
int iVal = 8;
//iVal == 8
if (iVal == 8){
int iVal = 5;
//iVal == 5
}
//iVal == 8
然而,在C#中你会得到一个编译器错误:
int iVal = 8;
if(iVal == 8) {
int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}
public static void Scope() {
int iVal = 8;
if(iVal == 8) {
int iVal2 = 5;
}
}
以下是生成的IL代码。请注意,定义在if块内的iVal2实际上是在函数级别上定义的。这实际上意味着,就变量生命周期而言,C#只有类和函数级别的作用域。
.method public hidebysig static void Scope() cil managed
{
// Code size 19 (0x13)
.maxstack 2
.locals init ([0] int32 iVal,
[1] int32 iVal2,
[2] bool CS$4$0000)
//Function IL - omitted
} // end of method Test2::Scope
在C++中,只要分配在堆栈上的变量超出其作用域范围,它就会被销毁。请记住,在C++中,您可以在堆栈或堆上创建对象。当您在堆栈上创建对象时,一旦执行离开作用域,它们就会从堆栈中弹出并被销毁。
if (true) {
MyClass stackObj; //created on the stack
MyClass heapObj = new MyClass(); //created on the heap
obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives
在CLR中,对象(即引用类型)总是在托管堆上创建。这一点还通过对象创建语法得到了进一步强化。考虑以下代码片段。
MyClass stackObj;
MyClass
实例并调用其默认构造函数。在C#中,它将创建一个不指向任何内容的类MyClass
的引用。创建类的实例的唯一方法是使用new
运算符:MyClass stackObj = new MyClass();
new
语法创建的对象非常相似——它们在堆上创建,但与 C++ 对象不同的是,它们由运行时管理,因此您不必担心对其进行析构操作。.NET GC使用一种混合的分代GC和标记-清除方法。分代方法涉及运行时优先检查最近分配的对象,因为它们更有可能未被使用;而标记-清除方法则需要运行时遍历整个对象图,并确定是否有未使用的对象组。这可以很好地处理循环依赖问题。
此外,.NET GC在另一个线程(所谓的终结器线程)上运行,因为它要做的事情相当多,如果在主线程上执行会中断您的程序。
正如其他人所说,如果类实现了IDisposable
,你肯定要调用Dispose
。我对此持有非常严格的立场。有些人可能会声称在DataSet
上调用Dispose
是毫无意义的,因为他们分解它并发现它不执行任何有意义的操作。但是,我认为这种论点中存在着谬误。
请阅读此处一位备受尊敬的人士就此问题进行的有趣辩论。随后再阅读我的理由此处,我认为Jeffery Richter所在的阵营错误。
现在,我们来讨论是否应该将引用设置为null
。答案是否定的。以下代码将说明我的观点。
public static void Main()
{
Object a = new Object();
Console.WriteLine("object created");
DoSomething(a);
Console.WriteLine("object used");
a = null;
Console.WriteLine("reference set to null");
}
那么当您认为由a
引用的对象可以被收集时,您认为是什么时候呢?如果您说在调用a = null
之后,那么您是错的。如果您说是在Main
方法完成之后,那么您也是错的。正确答案是它在调用DoSomething
期间的某个时间是可以被收集的。没错,它甚至可以在引用被设置为null
以前,甚至可能在调用DoSomething
完成之前就可以被回收了。这是因为JIT编译器可以识别对象引用何时不再被解除引用,即使它们仍然具有根引用。a
是类中的私有成员变量,如果a
没有被设置为null,GC就无法知道a
是否会在某个方法中再次被使用,对吧?因此,除非整个包含该变量的类被收集,否则a
不会被垃圾回收器收集,对吗? - Kevin P. Ricea
是一个类成员,并且包含a
的类仍然有根并且正在使用,则a
也将存在。在这种情况下,将其设置为 null
可能是有益的。 - Brian GideonDispose
,那么我有可能会在已经被处理的对象上尝试使用它,因为不能保证GC在再次使用之前将字段置空。” - JamesIDisposable
接口,那么是的,你应该对其进行处理。这个对象可能会挂起一些本地资源(文件句柄,操作系统对象),否则这些资源可能无法立即释放。这可能导致资源匮乏、文件锁定问题和其他一些微妙的错误,否则这些问题可以避免。Dispose
将被调用。此外,如果您的对象持有稀缺资源或锁定某些资源(例如文件),则应尽快释放它。等待垃圾回收器完成这项工作是次优的选择。 - Chris Schmich我同意这里的普遍答案,即您应该处理而不是通常不应将变量设置为null...但我想指出的是,dispose并不主要涉及内存管理。是的,它可以帮助(有时会),但它的主要目的是为您提供确定性释放稀缺资源。
例如,如果您打开硬件端口(例如串行端口),TCP / IP套接字,以排他访问模式打开文件,甚至是数据库连接,则现在已经防止任何其他代码使用这些项,直到它们被释放。 dispose通常释放这些项目(以及GDI和其他“操作系统”句柄等,虽然有数千个可用,但整体上仍然有限)。如果您不在所有者对象上调用dipose并明确释放这些资源,然后尝试在未来打开相同的资源(或另一个程序这样做),则该打开尝试将失败,因为您未处理,未收集的对象仍然保持项目的打开状态。当然,在GC收集该项时(如果正确实现了Dispose模式),资源将被释放...但您不知道那将是什么时候,因此您不知道重新打开该资源是否安全。这是Dispose解决的主要问题。当然,释放这些句柄通常也会释放内存,并且从不释放它们可能永远不会释放那些内存...因此所有关于内存泄漏或内存清理延迟的讨论。
我看到过这个问题在现实世界中引起问题。例如,我曾经见过ASP.Net Web应用程序最终无法连接到数据库(虽然仅短时间或直到Web服务器进程重新启动),因为sql server“连接池已满”...也就是说,已经创建了很多连接,并且在短时间内未显式释放,因此不能创建新连接,并且池中的许多连接虽然不活动,但仍然由未处理和未收集的对象引用,因此无法重新使用。必要时正确处理数据库连接可以确保不会发生此问题(除非您有非常高的并发访问)。
如果它们实现了IDisposable接口,则应该对它们进行处理。垃圾回收器将处理其余部分。
编辑:最好在使用可处理项时使用using
命令:
using(var con = new SqlConnection("..")){ ...
IDisposable
接口时,你应该调用 Dispose
方法(或者在某些情况下调用 Close
方法,因为它会自动调用 Dispose
方法)。null
,因为垃圾回收机制会知道对象不再被使用。null
,因为垃圾回收机制不知道我已经完成了它们的处理。using (var db = GetDatabase()) {
// Retrieves array of keys
var keys = db.GetRecords(mySelection);
for(int i = 0; i < keys.Length; i++) {
var record = db.GetRecord(keys[i]);
record.DoWork();
keys[i] = null; // GC can dispose of key now
// The record had gone out of scope automatically,
// and does not need any special treatment
}
} // end using => db.Dispose is called
Dispose()
方法中,您可能需要将某些引用设置为 null!这是对这个问题微妙的变化,但很重要,因为被处理的对象不知道它是否会“超出范围”(调用Dispose()
并不能保证)。更多信息请参见:https://dev59.com/Mmw15IYBdhLWcg3wMpEY - Kevin P. Rice