Programming .NET Components 2nd 学习笔记(五)

来源:互联网 发布:网络生鲜超市 编辑:程序博客网 时间:2024/05/22 04:59

4.4. Object Finalization

.NET对象在变成垃圾时不会告诉你;它们仅仅在托管堆压缩时被重写。这提出了一个问题:假如这个对象使用了昂贵的资源(文件、连接、通信端口、数据结构、同步锁等等),它怎么处理并释放这些资源?为了解决这个问题,.NET提供了对象终止化。假如一个对象有特殊的清除工作,它应该实现Finalize()方法,定义如下:

protected void Finalize(  );

 

当垃圾回收器决定一个对象为垃圾时,它会检查该对象的元数据。如果该对象实现了Finalize()方法,垃圾回收器就不会销毁该对象。与之替代的,垃圾回收器会标记该对象为可到达(因此该对象不会在压缩堆时被覆盖),然后将该对象从对象图中移到一个叫终止化队列的特殊队列。这个队列实质上只是另一个对象图,而且这个队列的根节点使这个对象可到达。接下来垃圾回收器开始回收垃圾并压缩堆。同时,一个单独的线程遍历终止化队列里的所有对象,调用每个对象的Finalize()方法使其进行清理工作。调用Finalize()方法后,垃圾回收器将该对象从终止化队列移除。

 

4.4.1. Explicit Garbage Collection

你可以使用System命名空间里的GC类的静态方法Collect()来明确触发垃圾回收:

public static class GC{    public static void Collect(  );    /* Other methods and members */}

 

然而,我建议尽量避免明确指示垃圾回收。垃圾回收是一个消耗昂贵的操作,它包含了扫描对象图,线程上下文交换,线程挂起和重新启动,潜在的硬盘存取,和大量运用发射去读取对象的元数据。通常,发起垃圾回收的原因是你想要确认对象的Finalize()方法被调用,处理该对象拥有的资源。替代发起垃圾回收,你可以使用在后面提出的确定性终止化。

 

Increasing Memory Pressure

通常,在托管堆耗尽时会触发垃圾回收。垃圾回收器监视着托管堆,当内存使用超出临界值时,触发垃圾回收。GC类提供了AddMemoryPressure()方法去降低这个临界值,使回收更加频繁:

public static class GC{    public static void AddMemoryPressure(long pressure);    public static void RemoveMemoryPressure(long pressure);    /* Other methods and members */}


你同样可以通过RemoveMemoryPressure()方法移除已增加的压力,但是你只能移除你通过AddMemoryPressure()方法明确增加的压力,不能超出这个值。

 

有两种可能会用到增加内存压力的情况。第一种是处理使用大量资源而自身占用内存较低的对象。增加内存压力会使回收更频繁同时也可能会回收昂贵的对象,这些对象会调用Finalize()方法去释放资源。问题在于很难给出具体增加多少压力,并且这样做不一定每次都能得到有效的结果。我建议在这种情况下避免使用增加内存压力,取而代之的使用确定性终止化。

 

第二种用法是在压力测试时。如果你想测试在紧张垃圾回收环境下你的应用程序功能,AddMemoryPressure()提供了一种简单的实现方法。这样的用法是可接受的。

 

你还可以使用System.Runtime.InteropServices命名空间内HandleCollector帮助类来触发垃圾回收:

public sealed class HandleCollector{    public HandleCollector(string name,int initialThreshold,int maximumThreshold);    public HandleCollector(string namt initialThreshold);       public void Add(  );    public void Remove(  );      public int InitialThreshold{get;}    public int MaximumThreshold{get;}    public int Count{get;}    public string Name{get;}}

 

HandleCollector允许你追踪昂贵的未托管资源的分配,例如窗口或文件的句柄。你使用一个HandleCollector对象来处理各种类型的资源。HandleCollector是用来处理自身耗费内存较少但持有昂贵未托管句柄的对象。当你创建一个新的未托管资源,你可以调用HandleCollector类的Add()方法来监视该资源。当你在Finalize()方法里释放一个资源时,你可以调用HandleCollectorRemove()方法。在内部,HandleCollector维护了一个计数器,在调用Add()或Remove()时会增加或减少计数器。就像这样,HandleCollector对每个句柄类型运用了一个简单的引用计数器。当构造一个新的HandleCollector对象时,需要指定初始值和最大值。当已分配的资源数量低于初始值时,不会进行垃圾回收。如果你调用Add()方法且资源计数器超过了初始值(但依然低于最大值),垃圾回收可能会进行也可能不会,这基于自我协调探索。如果你调用Add()方法且资源计数器超过了最大值,垃圾回收始终会进行。

 

使用HandleCollector会遇到以下几个问题:

    · 临界值该如何取值?这些值可能会随着用户环境与时间的变化而变化。

    · 你的构件怎么知道其他应用程序没有使用相同的句柄?

    · 如果你触发回收,但维持句柄的对象并未被当做垃圾,你将会此次回收付出代价并且不会从中获益。

 

归根到底,使用HandleCollector是一种粗糙的最优化技术,就像大多数最优化技术一样,你应该避免使用它。用确定性终止化替代它。

 

4.4.2. Finalize( ) Method Implementation

对象终止化做的事情比我们看见的多。尤其应该注意调用Finalize()在时间上是不确定的。它可能会延期,从而导致对象继续持有资源并威胁到可伸缩性和应用程序性能。这里有很多中方法提供确定性终止化,将在后面提到。

 

结束本部分前,这里还有几点需要留心,是关于何时实现Finalize()方法的:

    · 当你实现Finalize()时,调用基类的Finalize()方法是非常重要的,给基类执行它自己的清除工作的机会:

protected void Finalize(  ){    /* Object cleanup here */    base.Finalize(  );}


注意.NET类型System.Object并未在Finalize()方法里做任何工作,你可以无视基类是否提供Finalize()方法而去始终调用它。

 

    · 确定将Finalize()定义为保护方法。避免将Finalize()定义为私有方法,因为这样会阻碍子类调用你的Finalize()方法。有趣的是,.NET使用反射来调用Finalize()方法,并不会受到其访问级别的影响。

   · 避免进行阻塞调用,因为你将组织终止化队列中的其他对象终止化直到你的阻塞调用返回。

   · 终止化不能依赖于线程相关性来做清理工作。线程相关性是构件设计师的设想——构件的一个实例总是能在相同的线程上运行(虽然不同的对象能在不同的线程上运行)。Finalize()会在一个垃圾回收的线程上被调用,而不会在其他任何用户线程上被调用。因此,你不可能获取到任何线程相关的资源,例如线程本地存储、线程相关的静态变量。

   · 终止化不能依赖于特殊的顺序(例如:对象A必须在对象B释放资源后才能释放自己持有的资源)。这两个对象可能以任何顺序加入到终止化队列。

   · 调用基类实现的Finalize()方法是非常重要的,在遇到异常时也应如此。你可以将调用放到try/finally语句块中:

protected virtual void Finalize(  ){    try    {       /* Object cleanup here */    }    finally    {       base.Finalize(  );    }}

 

因为这些要点应广泛应用到每个类,所以C#编译器内置支持生成Finalize()代码。在C#中,你不需要提供Finalize()方法;取而代之的,你需要提供析构函数。编译器将析构函数转换成Finalize()方法,包含在异常处理语句块中,并且自动调用基类的Finalize()方法。例如:一个C#类的定义:

public class MyClass{    public MyClass(  )    {}       ~MyClass(  )    {       //Your destructor code goes here    }}

 

下面是编译器真正生成的代码:

public class MyClass{    public MyClass(  )    {}       protected virtual void Finalize(  )    {       try       {          //Your destructor code goes here       }       finally       {          base.Finalize(  );       }    }}

 

如果你尝试同时定义析构函数和Finalize()方法,编译器将生成一个编译错误。当你试图明确调用基类的Finalize()方法时,你也会得到一个错误。最后,在一个类层次的环境下,如果所有的类拥有析构函数,编译器生成的代码将以从最底端的子类到最顶端的基类的顺序调用所有析构函数。



 

原创粉丝点击