Garbage Collection : Automatic Memory Management in the Microsoft .NET Framework 垃圾回收:在微软NET框架自动内存管理(一)

Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework

垃圾回收:在微软NET框架自动内存管理 (一)

原文地址: http://msdn.microsoft.com/zh-cn/magazine/bb985010(en-us).aspx

概要:

MicrosoftNET公共语言运行环境的垃圾收集器使开发者完全从追踪内存使用以及了解何时释放中解放了。 不过,如果您可能想了解如何运作, 第一部分介绍了如何分配和管理资源,然后给出一个详细的一步一步的工程说明垃圾回收算法。还讨论了在垃圾回收器运行时如何能够正确的回收资源 以及如何在一个对象不再使用的时候及时清理。

为您的应用程序做一个专门的资源管理是一项艰巨的,乏味的工作。他会分散你在要解决的实际问题上的注意力。如果存在一个为开发者简化那些令人心烦意乱的内存管理工作的机制,那岂不是非常美妙! 非常幸运,在Net中我们有垃圾回收机制(Garbage Collection)。

让我们回到了一分钟。 每个程序都使用一种或多种资源如:内存缓冲区,屏幕空间,网络连接,数据库资源等等。事实上,在一个面向对象的环境中,每个类型都标识某些资源为你的程序的可用资源。使用这些资源需要分配代表这些类型的内存,访问资源所需的步骤如下:

1 为代表这些资源的类型分配内存。

2 初始化内存以设置资源的初始状态,将资源设置为可用状态。

3 使用类型实例对象的成员访问资源(必要时是通过对象的副本)

4 设置资源的状态为空闲以清理资源。

5 释放内存

 

这个看似简单的模式一直是编程错误的主要来源之一。 毕竟,bug1 有多少次你会在不在需要使用资源时,你忘记释放内存,或是bug2 在释放资源后企图再次访问这个资源?

这两个bug是比大多数其他应用程序的错误严重,因为其后果将是:将会发生什么后果以及什么时候会发生这些后果通常是不可预测的。 至于其他的bug,当你看到你的应用程序行为不正常,你就修复它。 但是这两个错误,造成了资源泄漏(内存消耗)和对象损坏(不稳定),使您的应用程序执行时在不可预测的时间出现无法预见的运行状态或者方式。 事实上,有许多工具(如任务管理器,系统监视器ActiveXÂ ®控制,Compuware的的BoundsCheckerRationalPurify)是专门设计用来帮助开发人员找到这些类型的错误。

当我检查GC,你会发现它使开发者完全从追踪内存使用以及了解何时释放中解放了。 然而,垃圾收集器不知道内存中的代表指定类型类型的资源是什么。 这意味着垃圾收集器不知道如何去执行步骤四-设置资源的状态为空闲以清理资源。为了得到一个正确的资源来清理,开发人员必须编写的代码,知道如何正确地清理资源。在。NET框架内,开发人员在一个CloseDispose或者Finalize方法(我将在稍后介绍)中处理此代码。然而,正如你将在后面看到,垃圾收集器能决定何时自动调用此方法。

此外,许多类型代表的资源,不要求任何清理。 例如,可以很轻松的清除一个矩形资源,只需要在内存中清除它的left, right, width, height字段。 另一方面,一个代表了文件资源或网络连接资源的类型在销毁时将需要一些明确的清理代码。 我将解释如何正确的完成这一切。 现在,让我们来看看内存是如何分配,以及资源如何被初始化。

资源分配

MicrosoftNET通用语言运行时要求所有资源从托管堆中分配。 这是类似于C运行时堆,除了你从来没有从托管堆释放的对象但这些对象会在应用程序不需要的时候自动释放这一点。这当然,提出了一个问题:如何在托管堆中知道一个对象不再被应用程序使用?我会在短期内说明这一问题。

这里有几个今天使用的GC算法。 每个算法是一个特定的环境的微调版本,以提供最佳的性能。 本文集中讨论在由公共语言运行时使用GC算法。 让我们先从基本概念开始。

当一个进程被初始化时,运行时保留了一个连续的地址空间区域,里面的内存是没有分配给任何对象的。 此地址空间区域是托管堆。 托管堆还维护一个指针,我称他为NextObjPtr 这个指针指示在堆内的何处分配下一个对象。 初始化的时候,NextObjPtr是托管堆的基址(也就是托管堆的第一个地址)。

一个应用程序使用new运算符创建一个对象。 使用这个运算符首先必须确保预留区域有足够适合这个新对象所需大小的内存(必要时提交存储)。 如果适合,然后NextObjPtr指向堆中的对象,这个对象的构造函数被调用,而new运算符返回对象的地址。

 

图一:托管堆

这时候,NextObjPtr递增创建的对象的大小,以便它指向下一个将要被放进堆里的对象的位置。 1显示了一个托管堆组成的三个对象ABC,以及下一个对象被分配将被放置在NextObjPtr点(对象C后)。

现在让我们来看看如何在C运行时堆分配内存。 C运行时堆中为对象分配内存,需要借助一个数据结构链表。 一旦找到足够大的块,该块会被分割,链表节点的指针必须进行修改,以保证所有的资源不受影响。 对于托管堆,分配的对象只是意味着将一个值赋给一个指针——相比较而言这是极快的。事实上,从托管堆中分配一个对象和从一个线程堆栈分配内存几乎一样快!

到目前为止,这听起来好像托管堆由于它的速度和执行简单所有远远优于C运行时堆。 当然,托管堆存在这么多优势的前提是一个非常大的假设:地址空间和存储是无限的。 这个假设是(毫无疑问)可笑的,而且必须存在一个机制使托管堆在使用时符合这个假设。 这种机制被称为垃圾收集器。 让我们看看它是如何工作。

当应用程序调用new运算符创建对象时,可能没有足够的预留空间供其使用去分配这个对象。 堆通过给指针NextObjPtr加上新的对象的大小去检测。 如果NextObjPtr超出了地址空间区域的结尾,那么堆已满,一个内存收集(collection)必须执行。

在现实中,一个内存收集(collection)发生在第0代全满的时候。 简单地说,一个代就是为了提高性能由垃圾收集器执行的一个执行机制。 这个想法是,新创建的对象是新代的一部分,在应用程序创建生命周期中之前创建的对象在一个老代中。 分离带中的对象可以让垃圾收集器收集特定代,而不是收集在托管堆中的所有对象。 代会在本文的第2部分更详细地讨论。

垃圾收集算法

垃圾收集器会检查堆中是否有不再被应用程序使用的对象。 如果这样的对象存在,那么这些对象使用的内存可以被回收。(如果堆中没有足够的可用内存,new运算符将引发OutOfMemoryException。)垃圾收集器是如何知道应用程序是否正在使用某个对象? 正如你可能想象的,这不是一个简单的问题。

每个应用程序都有一个roots集合。 roots标示存储位置,是指在托管堆上的对象或设置为null的对象。 例如,应用程序所有的全局和静态对象的指针被认为是应用程序的roots的一部分。 此外,任何在线程的堆栈中的局部变量/参数对象指针被认为是应用程序的roots的一部分,任何包含指向托管堆对象的CPU寄存器也被视为应用程序的根的一部分。活跃的roots是由just-in-time (JIT) 编译器和公共语言运行时来维护,并取得了进入的垃圾收集器的算法。

当垃圾收集器开始运行,它使假设在堆中的所有对象都是垃圾。 换句话说,它假定应用程序的roots没有指向堆中的任何对象。 现在,垃圾收集器开始遍历roots,建设一个roots所有对象图(graph)。 例如,垃圾收集器可以找到一个全局变量,它指向堆内的一个对象。

2显示了一个分配了对象的堆,应用程序的roots直接引用对象ACDF。所有这些对象都成为图的一部分。当添加对象D时,收集器会提示这个对象指向对象H,对象H也会被添加进图。收集器接着会继续遍历roots,并递归查找对象。

注:需要解释的是为何递归查找,因为如果roots中没有存储对象H,而对象Droots中,并且对象D拥有一个指向H的引用,那么H也会被添加进图,接着会查找H,如果H中有指向对象I的引用,而I又没有被添加进图,添加I,接着遍历I。。。。一直到没有找到任何有效对象引用后,才会跳出递归,接着继续遍历roots

 

图二:在堆中分配的对象

一旦这个部分的图完成,垃圾收集器会检查下一个根,开始继续遍历查找对象。 垃圾收集器一个一个的遍历搜索对象,如果它试图将一个以前添加过的对象添加到图,垃圾收集器会停止对这个对象的继续搜索。 这有两个目的。 首先,它有助于提高效率,因为它不会检索一组对象多次。 第二,它可以防止引起死循环在某些环形列表的情况下。

一旦所有的根都被检查,垃圾收集器的图包含的所有对象以某种方式从应用程序的根可达集;不属于图中的任何对象是不能由应用程序访问到的,因此被视为垃圾。 现在的垃圾收集器线性遍历堆,寻找连续的垃圾对象块(现在被认为是自由空间)。 垃圾收集器然后在内存中向下转移非垃圾对象(使用标准的memcpy函数),消除在堆中所有的空白。 当然,移动在内存中的对象会使原有指针指向无效的对象。 因此,垃圾收集器必须修改应用程序的根,使指针指向对象的新位置。 此外,如果一个对象包含指向另一个对象的指针,垃圾回收器负责纠正这些指针。 3显示了经历了一个收集(collection)后的托管堆。

 

图三:内存收集后的托管堆

当所有的垃圾已被确定,所有非垃圾已被压缩,而所有的非垃圾指针已固定起来,NextObjPtr定位只是在最后一个非垃圾对象。这时尝试运行new操作符,应用程序的创建新资源请求会被正确执行。正如你可以看到,垃圾回收产生严重的性能问题,这是使用托管堆的主要缺点。但是,请记住,只有当托管堆已满,在此之前,托管堆速度明显快于一C运行时堆。运行时的垃圾回收器还提供了一些优化,大大提高垃圾收集的性能。我将讨论在第2部分这篇文章当我谈这些优化的后代。

   

这时有几个重要的事情要注意。 您不必再执行任何管理你的应用程序任何资源生命周期的代码。 并注意我在本文开始时讨论的两个bug不复存在。 首先,它是不可能泄漏资源,因为任何不能从你的应用程序的根可指向的资源都可以在某个时间点被收集。 第二,它是不可能访问一个被释放的资源,因为该资源如果是可到达的资源将不会被释放(如果可访问,垃圾收集器不会回收,对象就会存在)。 如果它无法访问,那么你的应用程序无法访问它。 在代码Figure 4演示了如何把资源分配和管理。

//Figure 4 Allocating and Managing Resources

class Application

{

    public static int Main(String[] args)

    {

      // ArrayList object created in heap, myArray is now a root

      ArrayList myArray = new ArrayList();

      // Create 10000 objects in the heap

      for (int x = 0; x < 10000; x++) {

         myArray.Add(new Object());    // Object object created in heap

      }

      // Right now, myArray is a root (on the thread's stack). So,

      // myArray is reachable and the 10000 objects it points to are also

      // reachable.

      Console.WriteLine(a.Length);

      // After the last reference to myArray in the code, myArray is not

      // a root.

      // Note that the method doesn't have to return, the JIT compiler

      // knows

      // to make myArray not a root after the last reference to it in the

      // code.

      // Since myArray is not a root, all 10001 objects are not reachable

      // and are considered garbage.  However, the objects are not

      // collected until a GC is performed.

   }

}

如果GC是如此之好,你可能会疑惑为什么ANSI C + +中不能用。 原因是,垃圾收集器必须能够识别应用程序的根,还必须能够找到所有的对象指针。 C + +的问题是,它允许将一个指针从一个类型转化到另一个类型,而且也没有办法知道指针是指什么类型。 在公共语言运行库,托管堆总是知道一个对象的实际类型,元数据信息用于确定哪些对象成员引用其他对象。

Finalization

垃圾收集器提供了一个额外的功能,您可以利用这个功能:Finalization  Finalization允许资源在其本身被执行回收之后慢慢的被清理。 通过使用Finalization,一个代表文件或网络连接的资源能够正确的自我清理在垃圾收集器决定清理内存时。

下面是内存中发生的情况的一个简单模拟:当gc检测到一个对象是垃圾,gc调用这个对象的Finalization方法(如果存在这个方法),然后回收这个对象的内存。例如,假设您有以下类型(在C#中):

public class BaseObj

    {

        public BaseObj()

        {

        }

        protected override void Finalize()

        {

            // Perform resource cleanup code here...

            // Example: Close file/Close network connection

            Console.WriteLine("In Finalize.");

        }

    }

现在你可以创建这个对象的一个实例:

BaseObj bo = new BaseObj();

在未来某个时候,垃圾收集器将确定该对象是垃圾。 当发生这种情况,垃圾收集器会看到类型具有Finalize方法,将调用该方法,在控制台窗口输出“In Finalize.”并且回收这个对象所使用的内存。

许多c++程序员习惯在析构函数和Finalize方法之间制造一个直接关联。然而,让我提醒你现在:对象的Finalize和析构有非常不同的语义,这是最好忘记过去的一切关于Finalize和析构之间的关联的想法。 托管对象从来没有析构时期。

当设计一个类,最好避免使用Finalize方法。 这有几个原因:

       Finalizable对象将会跃迁到老一代,从而增加内存的压力,并且影响垃圾回收器确定那个对象是垃圾。此外,所以直接或间接引用这个对象的对象都会被跃迁。带和跃迁将会在本文的第二部分讨论。

       具有Finalizable的对象分配时需要较长的时间。

       强制垃圾回收器执行Finalize方法可以显着降低性能。 请记住,每个对象的finalized都必须被调用。所以,如果我有10000对象数组,每个对象都必须调用它的Finalize方法。

       Finalizable对象可能引用其他(非Finalizable)对象,延长其非Finalizable对象生命周期不必要的。事实上,你可以考虑将一个Finalizable类型拆分成两个不同的类型:一个轻量型具有Finalize方法不引用任何其他对象,另一个单独的类型没有Finalize方法,引用其他对象。

       Finalize方法执行时你无法进行任何控制。 这个对象可能会占用某些资源,直到下一次资源垃圾回收器运行。

       当应用程序终止时,某些对象仍然可以连接,不会调用他们的Finalize方法。 如果后台线程在使用对象或者对象是在应用程序关闭或卸载AppDomain的时候创建,这可能发生。 此外,默认情况下,应用程序退出时可达的对象的Finalize方法不会被调用,以便应用程序可能很快终止。 当然,所有的操作系统资源将被回收,但在托管堆中的任何对象都无法正常进行清理。 您可以通过调用System.GC类型的RequestFinalizeOnShutdown方法来改变这个默认行为。 但是,你应该谨慎的使用这个方法,因为它调用此方法意味着你的类型是控制整个应用程序的一个策略(每次应用程序退出都会执行非可到达对象的Finalize方法)。

       对于Finalize方法的调用顺序,运行时无法了解。 例如,假设有一个对象,它包含一个内部对象的指针。 垃圾回收器检测到这两个对象是垃圾。 此外,说是内部对象的Finalize方法会第一个被调用。 现在,外部对象的Finalize方法是允许访问内部对象和调用方法,但内部对象已经Finalization,其结果可能是不可预知。 出于这个原因,我们强烈建议Finalize方法不能访问任何内部成员对象。

如果您确定您的类型必须实现Finalize方法,然后确保代码能尽快执行。 避免一切将阻止Finalize方法的行动,包括任何线程同步操作。 此外,如果你让任何异常跳出Finalize方法,系统只是认为Finalize方法返回,并继续调用其他对象的Finalize方法。(Finalize必须保证在任何情况下调用都能被正确执行)

当编译器生成一个构造函数代码,编译器会自动插入到相应基类的构造函数的调用。同样,当一个C + +编译器生成的析构函数的代码,编译器会自动插入到相应基类的析构函数调用。不过,正如我以前说过的,Finalize方法是不同于析构函数的。编译器没有关于Finalize方法的专业知识,所以编译器不会自动生成代码来调用基类的Finalize方法。如果你想要添加这个行为----而且频繁的使用—你必须显式调用你的基类Finalize方法在你的Finalize方法内部:

public class BaseObj

    {

        public BaseObj()

        {

        }

        protected override void Finalize()

        {

            Console.WriteLine("In Finalize.");

            base.Finalize();    // Call base type's Finalize

        }

    }

请注意,您通常会调用基类型的Finalize方法在派生类型的Finalize方法的最后声明中。 这使基对象尽可能长活着。 由于调用基类的Finalize方法是共同的,C#有一个语法,简化您的工作。 C#中,下面的代码

       class MyObject

    {

        ~MyObject()

        {

            ...;

        }

    }

导致编译器生成的代码:

      class MyObject

    {

        protected override void Finalize()

        {

            •••;

            base.Finalize();

        }

    }

请注意,此C#语法看起来相同的C + +语言的定义析构函数的语法。 但请记住,C#不支持析构函数。 不要让相同的语法愚弄你。

Finalization内部机制

从表面上看,似乎Finalization很简单:你创建一个对象,当对象被收集,该对象的Finalize方法被调用。 但是,在执行 Finalization的时候还有比这更多的操作。

当应用程序创建一个新对象,new运算符从堆中分配内存。 如果对象的类型包含Finalize方法,那么finalization队列中就会添加一个指向该对象的指针。 finalization队列是一个内部数据结构被垃圾收集器控制。 队列中的每一项所指向的对象,都有它的Finalize方法,在该对象内存可以被回收之前调用。

 5显示了堆包括有几个对象。 这些对象有些是从应用程序的roots访问,并且有些则不能。 当对象C, E, F, I,J被创建时,系统检测到这些对象具有Finalize方法并且会添加指向这些对象的指针到finalization队列。

 

图五包括几个对象的堆

GC回收时,对象B, E, G, H, I,J被确定为垃圾。 垃圾收集器扫描完成队列中寻找到这些对象的指针。 当发现一个指针,从finalization队列中删除他并追加到freachable队列(称作"F-reachable")。 freachable队列是一个内部数据结构被垃圾收集器控制。 队列中的每个freachable指针标识一个对象,准备调用其Finalize方法。

经过收集,托管堆类似于图6 在这里,你看到的对象B, G,H占用的内存已被清理,因为这些对象没有一个Finalize方法,需要被调用。 然而,对象E, I,J所占用的内存不会被回收,因为他们的Finalize方法还没有被调用。

 

图六:执行垃圾回收后的托管堆

有一个特殊的运行线程专门调用Finalize方法。 freachable队列为空(这是通常的情况),这个线程会休眠。 但是,当队列不为空时,该线程被唤醒,从队列中删除每个条目,并调用每个对象的Finalize方法。 正因为如此,你不应该执行任何Finalize方法,这个方法应假设可使任何线程的执行代码(不确定哪个线程会执行这段代码,所以应该尽量健壮)。 例如,避免在Finalize方法访问线程本地存储。

finalization队列和freachable队列交互相当好。 首先,让我来告诉你如何freachable队列为什么叫这个名字。 f是明显的指的是finalizationfreachable队列中的每个条目应有的Finalize方法调用。 在“reachable的名称的一部分意味着对象是可达的(是指对象可以通过App根得到)。 换一种方式,freachable队列一样被认为是根就像全局和静态变量一样。 因此,如果一个对象在freachable队列,那么对象是可达的,而不是垃圾。

总之,当一个对象不可达,垃圾回收器认为该对象的垃圾。 然而,当垃圾收集器移动一个对象从finalization队列到freachable队列,该对象不再被视为垃圾,它的内存不回收。这时垃圾收集器已完成识别垃圾。 认定为垃圾的对象,有的已重新归类为不是垃圾(比如从freachable队列转移到freachable队列的对象)。 垃圾收集压缩可回收的内存,特殊的运行时线程清空freachable队列,执行每一个对象的Finalize方法。

下一次垃圾收集器被调用时,它看到的finalized对象是真正的垃圾,因为应用程序的根不再指向它,freachable队列不再指向它。现为对象的内存仅仅是回收。理解此处最重要的是:对于回收需要finalization的对象的内存时,必须要使用两次垃圾回收。事实上,多余两次的回收也是很正常的,因为对象可能会跃迁到老的代上去。图七显示两次垃圾回收之后的托管堆是什么情形:

Figure 7 Managed Heap after Second Garbage Collection

图七 两次垃圾回收后的托管堆 

复活

Finalization这个概念是正确的,但是,他比我上面所描述的这么多还要复杂的多。你会注意到在上一节,当一个应用程序不再访问活动对象,垃圾收集器的对象被认为死了。但是,如果对象需要最后需要Finalization,该对象被视为活一遍,直到它实际上是最后执行Finalize,然后它被永久死亡。换句话说,一个对象需要Finalization死亡,活着,然后再次死掉。他是一个非常有趣的现象,叫做复活。就像他的名字一样,允许一个对象由死到活。

 我已经描述了一个复活的形式。当垃圾回收器给freachable队列上放置一个对象的引用,该对象是从根访问,并已重新活过来。最终,该对象的Finalize方法被调用,无根指向的对象,之后对象永远死掉。但如果一个对象的Finalize方法执行的代码存在一个全局或静态变量对象的指针?

  public class BaseObj

            {

                protected override void Finalize()

                {

                    Application.ObjHolder = this;

                }

            }

            class Application

            {

                static public Object ObjHolder;    // Defaults to null

                •••

            }

在这种情况下,当该对象的Finalize方法执行,应用程序的一个根中存放着一个指向该对象的指针,该对象可以从应用程序的代码访问到。这个对象现在复活而垃圾回收器不会再将这个对象认为是垃圾。应用程序可以自由的使用这个对象,但是重要的是你要记得,这个对象已经被执行了finalized,使用这个对象会造成不可预知的结果。另外需要注意的是:如果BaseObj包含一个指向其他对象的成员(直接或间接),所有的这些对象都会被复活,因为他们都是可以从应用程序的根reachable的。但是,要知道,这些其他一些对象可能已经finalized

事实上,在设计自己的对象类型,您的类型的对象状态可能变成finalizedresurrected完全超出你的控制。为了正确处理这种情况,需要实现相应的代码。对于很多类型,这意味着保持一个布尔标志,指示对象是否已经finalized或者没有。然后,如果你的方法被调用一个已经finalized的对象,你可以考虑抛出一个异常。确切的技术的使用取决于你的类型。

现在,如果其他一些代码块设置Application.ObjHoldernull,对象是不可访问的。最终垃圾回收器会认为对象变成了一个垃圾,将会回收对象的存储空间。注意对象的Finalize方法不会被调用,因为没有此对象的指针在finalized队列中存在。

使用复活很少带来良好的效应,你应该尽量避免使用它。我们尽量希望对象可以在其死亡的时候正确的被清除。要做到这一点,GC的类型提供一个ReRegisterForFinalize方法,该方法接受一个参数:一个对象的指针。

  public class BaseObj

          {

            protected override void Finalize()

            {

                Application.ObjHolder = this;

                GC.ReRegisterForFinalize(this);

            }

         }

当这个对象的Finalize方法被调用,它通过制造一个指向指向对象的根来复活。Finalize方法然后调用ReRegisterForFinalize,它追加指定对象的地址(this)finalization的末尾。当垃圾回收器再次觉察到这个对象不可到达,对象的指针会转移到freachable队列,而Finalize方法会再次被调用。这个特殊的例子表示如何创建一个不断复活并且不会死去的对象,这通常是不可取的。这是更为常见,有条件地设置root引用Finalize方法里的对象。

必须确定每次复活仅仅调用ReRegisterForFinalize一次,或该对象Finalize方法被多次调用。这些发生的原因是每次调用ReRegisterForFinalize都会添加一个新的条目在finalization队列尾部。当一个对象被认为是一个垃圾,所有的这些条目从finalization队列转移到freachable队列,调用对象的Finalize多次。

强制清理对象

   如果可以的话,你应该尝试定义对象不要求任何清理。不幸的是,许多对象,这是根本不可能的。因此,对这些对象,必须实现一个Finalize方法,作为该类型的定义的一部分。但是,它也建议你增加一个额外的方法的类型,允许用户在想要清理的对象的时候明确的执行。按照惯例,这种方法应该调用CloseDispose

一般来说,如果您一般使用Close,在一个对象可以在close之后重开或重复使用。你还可以使用Close在通常被认为可关闭的对象上,例如文件。另一方面,你使用Dispose在那种被释放后不再使用的对象上,例如,要删除一个System.Drawing.Brush对象,调用它的Dispose方法。一旦销毁,画笔对象不能用了,调用方法来操纵对象可能会导致异常的抛出。如果您需要与另一个Brush,你必须建立一个新的Brush对象。

现在,让我们看看Close/ Dispose方法应该做的。在System.IO.FileStream类型允许用户打开并读取和写入文件。为了提高性能,该类型的实现使用一个内存缓冲区。只有当缓冲区填满才会刷新缓冲区的内容到文件。比方说,您创建一个新的FileStream对象并且只写几个字节的信息给它。如果这些字节不能填满缓冲区,缓冲区则没有写入到磁盘。 FileStream类型实现Finalize方法,当FileStream对象被回收时,Finalize方法从内存刷新所有剩余数据到磁盘,然后关闭该文件。

 但是,对于FileStream类型的用户来说这种方法可能不够好。比方说,第一个FileStream对象还没有被回收,但应用程序要创建一个新的FileStream对象使用相同的磁盘文件。在这种情况下,如果第一个FileStream对象已进行独占访问打开该文件,第二个FileStream对象将无法打开该文件。使用FileStream对象的用户必须有某种方式来强制将内存中的剩余数据刷新到磁盘,并关闭该文件。

但现在出现一个有趣的问题: FileStreamFinalize方法应该在FileStream对象被回收的时候做什么?显然,答案是什么也不做。事实上,如果在所有的应用程序显式调用Close方法,FileStreamFinalize方法不需要执行。你知道Finalize方法是不推荐的,且在这种情况下系统调用Finalize方法什么也不做。看起来,就必须有一种方法来抑制系统的对象的Finalize方法调用。幸运的是,有。System.GC类型包含一个静态方法,SuppressFinalize,这需要一个参数,一个对象的地址。

     8显示的FileStream的类型的实现。当你调用SuppressFinalize,它打开一个标志位与对象关联。当此标志启用时,运行时知道不移动这个对象的指针到freachable队列,防止该对象的Finalize方法被调用。

    //Figure 8 FileStream's Type Implementation

    public class FileStream : Stream

    {

        public override void Close()

        {

            // Clean up this object: flush data and close file

            •••

            // There is no reason to Finalize this object now

            GC.SuppressFinalize(this);

        }

        protected override void Finalize()

        {

            Close();    // Clean up this object: flush data and close file

        }

        // Rest of FileStream methods go here

        •••

    }

让我们来看看另一个有关的问题。这是很常见的使用一个FileStream对象和StreamWriter对象。

FileStream fs = new FileStream("C://SomeFile.txt",

            FileMode.Open, FileAccess.Write, FileShare.Read);

            StreamWriter sw = new StreamWriter(fs);

            sw.Write("Hi there");

            // The call to Close below is what you should do

            sw.Close();

            // NOTE: StreamWriter.Close closes the FileStream. The FileStream

            //       should not be explicitly closed in this scenario

请注意的StreamWriter的构造函数接受一个FileStream对象参数。 在内部, StreamWriter对象保存的FileStream指针。 这些对象都具有内部数据缓冲区,当您完成访问文件时应刷新缓冲区数据到文中。 调用的StreamWriterClose方法将最后的数据写入到FileStream,内部调用的FileStreamClose方法,它最终将数据写入到磁盘文件,并关闭该文件。 由于的StreamWriterClose方法关联关闭FileStream对象,因此你不应该自己去调用fs.Close

如果你删除了两次调用Close,你觉得会发生什么? 那么,垃圾回收器会正确地检测到这两个对象都是垃圾和将会finalized 但是,垃圾回收器不会保证在其中Finalize方法的调用顺序。 因此,如果FileStream得到首先finalized,关闭文件。 然后,当StreamWriter执行finalized时,它会试图将数据写入到已经关闭的文件,引发异常。 当然,如果StreamWriter首先finalized,那么数据将被安全地写入文件。

微软是如何解决这个问题呢? 使垃圾收集器按照一个特定的顺序finalized对象是不可能的,因为每个对象可能包含其他对象的指针也没有办法让垃圾收集器猜中这些对象正确的finalized顺序。 所以,微软的解决方案是:StreamWriter类型不实现Finalize方法。 当然,这意味着忘记关闭StreamWriter对象肯定会丢失数据。 微软希望开发人员看到这样的数据丢失,然后显式调用Close方法来修复这些问题。

如前所述,SuppressFinalize方法只设置一个标志位表明该对象的Finalize方法不应该被调用。 不过,运行时会在决定调用Finalize方法的时候重置这个标志。 这意味着,调用ReRegisterForFinalizeSuppressFinalize无法进行匹配。 在代码Figure 9显示我的意思是正确的。

//Figure 9 ReRegisterForFinalize and SuppressFinalize

        void method()

        {

            // The MyObj type has a Finalize method defined for it

            // Creating a MyObj places a reference to obj on the finalization table.

            MyObj obj = new MyObj();

            // Append another 2 references for obj onto the finalization table.

            GC.ReRegisterForFinalize(obj);

            GC.ReRegisterForFinalize(obj);

            // There are now 3 references to obj on the finalization table.

            // Have the system ignore the first call to this object's Finalize

            // method.

            GC.SuppressFinalize(obj);

            // Have the system ignore the first call to this object's Finalize

            // method.

            GC.SuppressFinalize(obj);   // In effect, this line does absolutely

                                        // nothing!

            obj = null;   // Remove the strong reference to the object.

            // Force the GC to collect the object.

            GC.Collect();

            // The first call to obj's Finalize method will be discarded but

            // two calls to Finalize are still performed.

        }

ReRegisterForFinalizeSuppressFinalize是为了提高性能而实现的。 只要每次调用SuppressFinalize时有调用ReRegisterForFinalize,一切正常。 但是你需要确保不多次调用ReRegisterForFinalizeSuppressFinalize,或者对一个对象的Finalize方法的多次调用。

结论

垃圾收集环境的产生的原因是为了简化开发人员的内存管理。 本概述第一部分介绍了一些一般性的GC概念和内部结构。 在第二部分中,我将结束这一讨论。 首先,我将探讨所谓WeakReferences的功能,您可以强行加载在大对象上以减少托管堆内存压力。 然后,我将考察一个机制,允许您以人为延长一个托管对象的生命周期。 最后,我将通过讨论总结的垃圾收集器各个方面的性能。 我将讨论代,多线程收集,公共语言运行时公开的性能计数器,它允许您监控垃圾收集器的实时行为。

 背景资料请参见:

Garbage Collection: Algorithms for Automatic Dynamic Memory Management by Richard Jones and Rafael Lins (John Wiley & Son, 1996)

Programming Applications for Microsoft Windows by Jeffrey Richter (Microsoft Press, 1999)

 Jeffrey Richter Programming Applications for Microsoft Windows (Microsoft Press, 1999)的作者。 他擅长的。NETWin32编程/设计。  杰夫是目前正在写的MicrosoftNET框架编程的书和优惠。NET技术研讨会。

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值