垃圾回收和资源释放






2.06/5 (6投票s)
2004年11月2日
5分钟阅读

62525

2
一篇关于 .NET 中的 IDisposable 和垃圾回收器的文章。
引言
在 C++ 等非垃圾回收语言中,程序员必须负责内存和资源管理,你确切地知道对象的销毁时间,无论是隐式(作用域规则)还是显式(对象删除)。与之相对的是,使用指针通常会导致内存泄漏和资源未释放。相反,.NET 环境实现了垃圾回收器,可以自动管理内存并处理未使用的对象销毁,从而简化了程序员的生活。对象不会在其超出作用域时销毁,你也无法显式销毁它们。销毁它们并释放所用内存是 GC 的工作,这被称为“回收”,并且在某些此处未解释的情况下进行。这意味着 GC 在需要时进行回收,你无法控制对象的销毁。
因此,对于不使用资源的类,编写时无需特别考虑。但是,当你的类持有需要释放的关键资源时,你不能依赖 GC,因为你无法确定它何时会运行。GC 在回收时会调用对象的终结器(在 C# 中,终结器是对象的析构函数),因此,这不是释放对象所拥有资源的好地方。在 .NET 类库中,垃圾回收器作为 System.GC
实现,位于 mscorlib
程序集中。
实现 IDisposable
在 .NET 中有一个接口,你必须实现它来“标记”你的类,以便提供一个释放资源的机制。这个接口是 IDisposable
(全名为 System.IDisposable
,位于 mscorlib
程序集中)。它只有一个你必须实现的方法:Dispose()
。此方法在你的类中执行清理工作。析构函数应该调用 Dispose()
,以确保在你忘记显式调用它时能够干净地退出,但如果已经调用过则不调用。建议的实现方式是
public class A : IDisposable
{
protected bool disposed = false;
public A()
{
// acquire resource
}
~A()
{
Dispose(false);
}
public void doSomething()
{
if (disposed)
throw new ObjectDisposedException("object's name");
else
{
// do something
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Call Dispose() on managed objects
}
// release unmanaged resource(s) held by this object
disposed = true;
}
}
}
一个虚拟方法 Dispose(bool)
用于管理托管和非托管资源的释放。disposing
参数用于告知谁调用了该方法。Dispose()
将调用 Dispose(true)
,而析构函数将调用 Dispose(false)
。实例化类 A
时,所有资源的获取都在构造函数中完成,当我们完成对象的使用后,需要从公共方法 Dispose()
调用 Dispose(true)
来释放所有(托管和非托管)资源。GC 调用的析构函数也会调用 Dispose(false)
来清理非托管资源,以确保我们的应用程序中没有泄漏。调用 GC.SuppressFinalize()
告诉 GC 不要调用对象的析构函数,因为它已经完成了我们已经做过的事情。Dispose(bool disposing)
声明为 virtual
,因此我们的派生类可以调用它。disposed
是一个标志,用于避免多次调用 Dispose()
并避免使用已释放的资源,如果发生这种情况,将抛出 ObjectDisposedException
。请注意,此实现不是线程安全的,可能会发生竞态条件。
实现 Dispose(bool)
的必要性源于一个对象可能同时持有需要释放的托管对象和非托管资源。因此,如果你调用 Dispose(true)
,你将负责所有拥有的资源,仅此而已,但如果你不这样做,GC 将会负责,而所有者对象不应再对每个拥有的托管对象调用 Dispose(true)
,因为它们可能已经被终结。但你必须始终通过调用 Dispose(false)
来清理非托管资源。
派生类可以轻松地扩展此实现
public class B : A
{
public B()
{
// acquire more resources
}
~B()
{
}
protected override void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Call Dispose() on our managed objects
}
// release unmanaged resources acquired in our constructor
base.Dispose(disposing);
}
}
}
创建 B
的实例会调用 B
的构造函数,后者在退出前会调用 A
的默认构造函数。B
的析构函数不做任何事情,它只遵循销毁链调用 A
的析构函数(实际上,我们可以省略它,当 GC 回收对象时,它会调用 A
的析构函数,因为 B
继承了它)。无论如何,我们都会到达 A
的析构函数,在那里会调用 Dispose(false)
。但 Dispose(bool)
被重写了,它在 A
中被声明为 virtual
,因此在 A
的析构函数中会调用正确的 Dispose(bool)
。如果它没有被声明为 virtual
,则会调用 A.Dispose(bool)
,从而不会调用 B.Dispose(bool)
,也就永远不会释放 B
的非托管资源。
仔细看看我们的示例,Ildasm.exe 的输出显示
(当编译成 IL 时,~A()
被翻译为 A.Finalize()
)。
.method family hidebysig virtual instance void Finalize() cil managed
{
// Code size 17 (0x11)
.maxstack 2
.try
{
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: callvirt instance void Test.A::Dispose(bool)
IL_0007: leave.s IL_0010
} // end .try
finally
{
IL_0009: ldarg.0
IL_000a: call instance void [mscorlib]System.Object::Finalize()
IL_000f: endfinally
} // end handler
IL_0010: ret
} // end of method A::Finalize
编译器会自动生成代码来控制异常,它将我们的代码放入一个 try
/finally
块中,确保在 finally
块中调用基类析构函数来完成链式销毁。此外,如果对 Dispose(bool)
的虚拟调用抛出了一个未处理的异常,程序将继续执行。如果在每个 Dispose()
方法中没有正确处理异常,可能会导致资源泄漏。
using 语句
C# 提供了 using
语句,允许你获取一个或多个资源,在块中使用它们,并自动调用每个资源的 Dispose()
。
public class AppClass
{
public static void Main()
{
using (B a = new B())
{
Console.WriteLine(a.ToString());
};
}
}
这段代码在 IL 中被翻译成如下
.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 30 (0x1e)
.maxstack 1
.locals init ([0] class Test.B a)
IL_0000: newobj instance void Test.B::.ctor()
IL_0005: stloc.0
.try
{
IL_0006: ldloc.0
IL_0007: callvirt instance string [mscorlib]System.Object::ToString()
IL_000c: call void [mscorlib]System.Console::WriteLine(string)
IL_0011: leave.s IL_001d
} // end .try
finally
{
IL_0013: ldloc.0
IL_0014: brfalse.s IL_001c
IL_0016: ldloc.0
IL_0017: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_001c: endfinally
} // end handler
IL_001d: ret
} // end of method AppClass::Main
这等同于编写
B a = new B();
try
{
Console.WriteLine(a.ToString());
}
finally
{
if (a != null)
a.Dispose();
}
using
也允许声明同一类型的多个标识符
using (B a1 = new B(), a2 = new B())
{
// do something
}
using
语句在语句块结束或抛出异常时执行语句块,并释放对象。
结论
GC 提供的非确定性对象销毁迫使我们在处理包装资源的类时要格外小心
- 实现
IDisposable
。IDisposable
提供了类必须实现的用于正确使用资源的契约。 Dispose()
调用Dispose(true)
来释放所有资源,并告知 GC 不要调用析构函数。- 将
Dispose(bool)
声明为virtual
,并从派生类调用继承的方法。 - 通过
using
语句确保调用Dispose()
。 - 在析构函数中调用
Dispose(false)
。以防万一…… - 多次调用
Dispose()
不应产生任何效果。 - 使用已释放的对象应该抛出
ObjectDisposedException
。