如何学习在.NET里的垃圾回收编程?
如何学习在.NET里的垃圾回收编程?
.NET 框架的垃圾回收器管理应用程序的内存分配和释放。每次您使用 new 运算符创建对象时,运行库都从托管堆为该对象分配内存。只要托管堆中有地址空间可用,运行库就会继续为新对象分配空间。但是,内存不是无限大的。最终,垃圾回收器必须执行回收以释放一些内存。垃圾回收器优化引擎根据正在进行的分配情况确定执行回收的最佳时间。当垃圾回收器执行回收时,它检查托管堆中不再被应用程序使用的对象并执行必要的操作来回收它们占用的内存。
开发人员在内存管理方面的背景
根据您开发背景的不同,您在内存管理方面的经验也会有所不同。在某些情况下,您可能需要让您的编程习惯来适应公共语言运行库提供的自动内存管理。
COM 开发人员
COM 开发人员习惯于将实现引用计数作为一个手动的内存管理技术。每次引用一个对象,计数器就递增。如果对对象的引用超出了范围,计数器就递减。当对象的引用计数达到零时,对象被终止并释放其内存。
引用计数方案会引发许多调试错误。如果未能严格地按照引用计数的规则进行操作,对象可能被过早释放或者未引用的对象积存在内存中。循环引用也是常见的问题根源。循环引用出现在子对象引用父对象,而父对象又引用子对象时。这种情况使两个对象都不能被释放或销毁。唯一的解决方案就是让父对象和子对象都遵守一个固定的使用和销毁模式,例如总是先由父对象删除子对象。
当使用托管语言开发应用程序时,运行库的垃圾回收器免除了对引用进行计数的需要,因此也就避免了由这种手动管理内存方案引发的错误。
C++ 开发人员
C++ 开发人员熟悉与手动内存管理相关的任务。在 C++ 中,当您使用 new 运算符为对象分配内存时,您必须使用 delete 运算符释放对象的内存。这可能导致多种错误,例如忘记释放对象、引起内存泄漏或试图访问已被释放的对象的内存。
当使用 C++ 的托管扩展或其他托管语言开发应用程序时,您就不必使用 delete 运算符释放对象了。垃圾回收器在当对象不再被应用程序使用时自动为您完成这些操作。
考虑到手动管理短期对象内存的相关成本,C++ 开发人员可能习惯于避免使用这些对象。对于两次回收间创建的然后又不再使用的托管短期对象,分配和释放内存的成本非常低。在 .NET 框架中,实际上已经对垃圾回收器进行了优化来管理具有较短生存期的对象。当开发托管应用程序时,在短期对象可以简化代码的情况下使用它们是非常合适的。
Visual Basic 开发人员
Visual Basic 开发人员习惯于自动内存管理。您熟悉的编程惯例将应用于您在 .NET 框架中创建的大多数托管对象。但是,当创建或使用封装非托管资源的对象时,您应该特别注意使用 Dispose 方法的推荐设计模式。
.NET 框架支持的托管语言比此处介绍的还要多。不管您使用哪一种托管语言,.NET 框架的垃圾回收器都提供自动内存管理。它为托管对象分配和释放内存,并在必要时执行 Finalize 方法和析构函数来适当地清理非托管资源。自动内存管理通过消除手动内存管理方案引起的常见问题简化了开发。
Finalize 方法和析构函数
对于您的应用程序创建的大多数对象,可以依靠 .NET 框架的垃圾回收器隐式地执行所有必要的内存管理任务。但是,在您创建封装非托管资源的对象时,当您在应用程序中使用完这些非托管资源之后,您必须显式地释放它们。最常见的一类非托管资源就是包装操作系统资源的对象,例如文件、窗口或网络连接。虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它不了解具体如何清理这些资源。对于这些类型的对象,.NET 框架提供 Object.Finalize 方法,它允许对象在垃圾回收器回收该对象使用的内存时适当清理其非托管资源。默认情况下,Finalize 方法不执行任何操作。如果您要让垃圾回收器在回收对象的内存之前对对象执行清理操作,您必须在类中重写 Finalize 方法。当使用 C# 和 C++ 的托管扩展以外的编程语言进行开发时,您可以实现 Finalize 方法。C# 和托管扩展提供析构函数作为编写终止代码的简化机制。析构函数自动生成 Finalize 方法和对基类的 Finalize 方法的调用。在 C# 和托管扩展编程语言中,您必须为终止代码使用析构函数语法。
垃圾回收器使用名为“终止队列”的内部结构跟踪具有 Finalize 方法的对象。每次您的应用程序创建具有 Finalize 方法的对象时,垃圾回收器都在终止队列中放置一个指向该对象的项。托管堆中所有需要在垃圾回收器回收其内存之前调用它们的终止代码的对象都在终止队列中含有项。
实现 Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。这时,它不能回收具有终结器的不可访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。一个特殊的运行库线程开始处于活动状态并调用列表中对象的 Finalize 方法,然后将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。
清理非托管资源
通过将对象的范围限制为 protected,您可以防止应用程序的用户直接调用对象的 Finalize 方法。除此之外,我们强烈建议您不要直接从应用程序代码中调用非基类的类的 Finalize 方法。为适当处置非托管资源,建议您实现公共的 Dispose 或 Close 方法,这两个方法执行必要的对象清理代码。IDisposable 接口为实现接口的资源类提供 Dispose method。因为 Dispose 方法是公共的,所以应用程序的用户可以直接调用该方法来释放非托管资源占用的内存。如果正确实现了 Dispose 方法,则 Finalize 方法(或者 C# 中的析构函数或 C++ 的托管扩展)就成为避免在没有调用 Dispose 方法的情况下清理资源的一种防护措施。
实现 Dispose 方法 [C#]
类型的 Dispose 方法应该释放它拥有的所有资源。它还应该通过调用其父类型的 Dispose 方法释放其基类型拥有的所有资源。该父类型的 Dispose 方法应该释放它拥有的所有资源并同样也调用其父类型的 Dispose 方法,从而在整个基类型层次结构中传播该模式。要确保始终正确地清理资源,Dispose 方法应该可以被多次安全调用而不引发任何异常。
Dispose 方法应该为它处置的对象调用 GC.SuppressFinalize 方法。如果对象当前在终止队列中,GC.SuppressFinalize 防止其 Finalize 方法被调用。请记住,执行 Finalize 方法会大大减损性能。如果您的 Dispose 方法已经完成了清理对象的工作,那么垃圾回收器就不必调用对象的 Finalize 方法了。
下面的代码示例旨在阐释如何为封装了非托管资源的类实现 Dispose 方法的一种可能的设计模式。因为该模式是在整个 .NET 框架中实现的,所以您可能会发现它十分便于使用。但是,这不是 Dispose 方法唯一可能的实现。
资源类通常是从复杂的本机类或 API 派生的,而且必须进行相应的自定义。使用这一代码模式作为创建资源类的一个起始点,并根据封装的资源提供必要的自定义。不能编译该示例,也不能将其直接用于应用程序。
在此示例中,基类 BaseResource
实现可由类的用户调用的公共 Dispose 方法。而该方法又调用 virtual Dispose(bool disposing) 方法(Visual Basic 中的虚 Dispose(作为布尔值处置))。根据调用方的标识传递 true 或 false。以虚 Dispose 方法为对象执行适当的清理代码。
Dispose(bool disposing) 以两种截然不同的方案执行。如果处置结果为 true,则该方法已由用户的代码直接调用或间接调用,并且可处置托管资源和非托管资源。如果处置结果为 false,则该方法已由运行库从终结器内部调用,并且只能处置非托管资源。因为终结器不会以任意特定的顺序执行,所以当对象正在执行其终止代码时,不应引用其他对象。如果正在执行的终结器引用了另一个已经终止的对象,则该正在执行的终结器将失败。
基类提供的 Finalize 方法或析构函数在未能调用 Dispose 的情况下充当防护措施。Finalize 方法调用带有参数的 Dispose 方法,同时传递 false。不应在 Finalize 方法内重新创建 Dispose 清理代码。调用 Dispose(false) 可以优化代码的可读性和可维护性。
类 MyResourceWrapper
阐释如何用 Dispose 从实现资源管理的类派生。MyResourceWrapper
重写 virtual Dispose(bool disposing) 方法并为其创建的托管和非托管资源提供清理代码。MyResourceWrapper
还对其基类 BaseResource
调用 Dispose 以确保其基类能够适当地进行清理。请注意,派生类 MyResourceWrapper
没有不带参数的 Finalize 方法或 Dispose 方法,因为这两个方法从基类 BaseResource
继承它们。
// Design pattern for the base class.
// By implementing IDisposable, you are announcing that instances
// of this type allocate scarce resources.
public class BaseResource: IDisposable
{
// Pointer to an external unmanaged resource.
private IntPtr handle;
// Other managed resource this class uses.
private Component Components;
// Track whether Dispose has been called.
private bool disposed = false;
// Constructor for the BaseResource Object.
public BaseResource()
{
// Insert appropriate constructor code here.
}
// Implement Idisposable.
// Do not make this method virtual.
// A derived class should not be able to override this method.
public void Dispose()
{
Dispose(true);
// Take yourself off of the Finalization queue
// to prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
// Dispose(bool disposing) executes in two distinct scenarios.
// If disposing equals true, the method has been called directly
// or indirectly by a user's code. Managed and unmanaged resources
// can be disposed.
// If disposing equals false, the method has been called by the
// runtime from inside the finalizer and you should not reference
// other objects. Only unmanaged resources can be disposed.
protected virtual void Dispose(bool disposing)
{
// Check to see if Dispose has already been called.
if(!this.disposed)
{
// If disposing equals true, dispose all managed
// and unmanaged resources.
if(disposing)
{
// Dispose managed resources.
Components.Dispose();
}
// Release unmanaged resources. If disposing is false,
// only the following code is executed.
CloseHandle(handle);
handle = IntPtr.Zero;
// Note that this is not thread safe.
// Another thread could start disposing the object
// after the managed resources are disposed,
// but before the disposed flag is set to true.
}
disposed = true;
}
// Use C# destructor syntax for finalization code.
// This destructor will run only if the Dispose method
// does not get called.
// It gives your base class the opportunity to finalize.
// Do not provide destructors in types derived from this class.
~BaseResource()
{
// Do not re-create Dispose clean-up code here.
// Calling Dispose(false)is optimal in terms of
// readability and maintainability.
Dispose(false);
}
// Allow your Dispose method to be called multiple times,
// but throw an exception if the object has been disposed.
// Whenever you do something with this class,
// check to see if it has been disposed.
public void DoSomething()
{
if(this.disposed)
{
throw new ObjectDisposedException();
}
}
}
// Design pattern for a derived class.
// Note that this derived class inherently implements the
// IDisposable interface because it is implemented in the base class.
public class MyResourceWrapper: BaseResource
{
// A managed resource that you add in this derived class.
private ManagedResource addedManaged;
// A native unmanaged resource that you add in this derived class.
private NativeResource addedNative;
private bool disposed = false;
// Constructor for this object.
public MyResourceWrapper()
{
// Insert appropriate constructor code here.
}
protected override void Dispose(bool disposing)
{
if(!this.disposed)
{
try
{
if(disposing)
{
// Release the managed resources you added in