C++ Programmer's Cookbook

{C++ 基础} {C++ 高级} {C#界面,C++核心算法} {设计模式} {C#基础}

.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 继承它们。

[C#] 
//  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 
//  this derived class here. 
addedManaged.Dispose(); 
}
 
//  Release the native unmanaged resources you added 
//  in this derived class here. 
CloseHandle(addedNative); 
this .disposed  =   true
}
 
finally  

//  Call Dispose on your base class. 
base .Dispose(disposing); 
}
 
}
 
}
 
}
 

//  This derived class does not have a Finalize method 
 对于finalize()方法的另一个问题是开发人员不知道什么时候它将被调用。它不像C++中的析构函数在删除一个对象时被调用。为了解决这个问题,在.Net中提供了一个接口IDisposable。微软建议在实现带有fianlize()方法的类的时侯按照下面的模式定义对象:


public class Class1 : IDisposable 
{
 
public Class1()
 
{
 }


 
~Class1 ()
 
{
  
//垃圾回收器将调用该方法,因此参数需要为false。
  Dispose (false);
 }


 
//该方法定义在IDisposable接口中。
 public void Dispose ()
 
{
  
//该方法由程序调用,在调用该方法之后对象将被终结。
  
//因为我们不希望垃圾回收器再次终结对象,因此需要从终结列表中去除该对象。
  GC.SuppressFinalize (this);
  
//因为是由程序调用该方法的,因此参数为true。
  Dispose (true);
 }


 
//所有与回收相关的工作都由该方法完成
 private void Dispose(bool disposing)
   
{
  
lock(this//避免产生线程错误。
  {
   
if (disposing)
   
{
    
//需要程序员完成释放对象占用的资源。
   }


  
//对象将被垃圾回收器终结。在这里添加其它和清除对象相关的代码。
 }

}

}




现在我们了解了垃圾回收器工作的基本原理,接下来让我们看一看垃圾回收器内部是如何工作的。目前有很多种类型的垃圾回收器。微软实现了一种生存期垃圾回收器(Generational Garbage Collector)。生存期垃圾回收器将内存分为很多个托管堆,每一个托管堆对应一种生存期等级。生存期垃圾回收器遵循着下面的原则:

  新生成的对象,其生存期越短;而对象生成时间越长的对象,其生存期也就越长。对于垃圾回收器来说,回收一部分对象总是比回收全部对象要快,因此垃圾回收器对于那些生存期短的对象回收的频率要比生存期长的对象的回收频率高。

  .Net中的垃圾回收器中目前有三个生存期等级:0,1和2。0、1、2等级对应的托管堆的初始化大小分别是256K,2M和10M。垃圾回收器在发现改变大小能够提高性能的话,会改变托管堆的大小。例如当应用程序初始化了许多小的对象,并且这些对象会被很快回收的话,垃圾回收器就会将0等级的托管堆变为128K,并且提高回收的频率。如果情况相反,垃圾回收器发现在0等级的托管堆中不能回收很多空间时,就会增加托管堆的大小。

在应用程序初始化的之前,所有等级的托管堆都是空的。当对象被初始化的时候,他们会按照初始化的先后顺序被放入等级为0的托管堆中。在托管堆中对象的存放是连续的,这样使得托管堆存取对象的速度很快,因为托管对不必对内存进行搜索。垃圾回收器中保存了一个指针指向托管堆中最后一个对象之后的内存空间。图一中显示了一个包含四个对象的0等级的托管堆。


图一 包含四个对象的托管堆

当0等级托管堆被对象填满后,例如候程序初始化了新的对象,使0等级托管堆的大小超过了256K,垃圾回收器会检查托管堆中的所有对象,看是否有对象可以回收。当开始回收操作时,如前面提到的,垃圾回收器会找出根节点和根节点直接或间接引用了的对象,然后将这些对象转移到1等级托管堆中,并将0等级托管堆的指针移到最开始的位置以清除所有的对象。同时垃圾回收器会压缩1等级托管堆以保证所有对象之间没有内存空隙。当1等级托管堆满了之后,会将对象转移到2等级的托管堆。

  例如在图一之后,垃圾回收器开始回收对象,假定D对象将被回收,同时程序创建了E和F对象。这时候托管堆中的对象如图二所示。


图二 回收对象后的0等级和1等级托管堆

  然后程序创建了新的对象G和H,再一次触发了垃圾回收器。对象E将被回收。这时候托管堆中的对象如图三所示。



  生存期垃圾回收器的原则也有例外的情况。当对象的大小超过84K时,对象会被放入"大对象区"。大对象区中的对象不会被垃圾回收器回收,也不会被压缩。这样做是为了强制垃圾回收器只能回收小对象以提高程序的性能。

  控制垃圾回收器

  在.Net框架中提供了很多方法使开发人员能够直接控制垃圾回收器的行为。通过使用GC.Collect()或GC.Collect(int GenerationNumber)开发人员可以强制垃圾回收器对所有等级的托管堆进行回收操作。在大多数的情况下开发人员不需要干涉垃圾回收器的行为,但是有些情况下,例如当程序进行了非常复杂的操作后希望确认内存中的垃圾对象已经被回收,就可以使用上面的方法。另一个方法是GC.WaitForPendingFinalizers(),它可以挂起当前线程,直到处理完成器队列的线程清空该队列为止。

  使用垃圾回收器最好的方法就是跟踪程序中定义的对象,在程序不需要它们的时候手动释放它们。例如程序中的一个对象中有一个字符串属性,该属性会占用一定的内存空间。当该属性不再被使用时,开发人员可以在程序中将其设定为null,这样垃圾回收器就可以回收该字符串占用的空间。另外,如果开发人员确定不再使用某个对象时,需要同时确定没有其它对象引用该对象,否则垃圾回收器不会回收该对象。

  另外值得一提的是finalize()方法应该在较短的时间内完成,这是因为垃圾回收器给finalize()方法限定了一个时间,如果finalize()方法在规定时间内还没有完成,垃圾回收器会终止运行finalize()方法的线程。在下面这些情况下程序会调用对象的finalize()方法:

   0等级垃圾回收器已满

   程序调用了执行垃圾回收的方法

   公共语言运行库正在卸载一个应用程序域

   公共语言运行库正在被卸载

posted on 2006-04-30 15:52 梦在天涯 阅读(2255) 评论(0)  编辑 收藏 引用 所属分类: C#/.NET


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


公告

EMail:itech001#126.com

导航

统计

  • 随笔 - 461
  • 文章 - 4
  • 评论 - 746
  • 引用 - 0

常用链接

随笔分类

随笔档案

收藏夹

Blogs

c#(csharp)

C++(cpp)

Enlish

Forums(bbs)

My self

Often go

Useful Webs

Xml/Uml/html

搜索

  •  

积分与排名

  • 积分 - 1795669
  • 排名 - 5

最新评论

阅读排行榜