GC:.net framework中的自动内存管理--part 1 (翻译)

哈,又翻译了一篇文章,Jeffrey Richter的GC内存管理。呼,翻译真是不容易啊,利用工作空闲时间,翻译了好几天。

 

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

GC:.net framework中的自动内存管理

 

Jeffrey Richter

 

本文假设你已熟悉C和C++

 

概要 :.net framework通用语言运行时环境完全将开发者从跟踪内存使用和何时释放内存中隔离出来。然而,你可能想了解它是如何工作的。
本文的第一部分解释了GC是如何分配和管理的资源的,然后一步一步的详细介绍GC算法是如何工作的。同时也讨论当GC决定去释放内存是
如何使资源正确释放的方法以及如何强制清除未引用的对象。

 

     在应用程序中引入合适的资源管理是一项困难而且乏味的任务。它会转移你真正想处理问题的注意力。如果有一些方法能够简化这些烦人的内存
管理问题肯定是件很爽的事情呢?幸运的是,.net就拥有这样的东西:GC

let's back up a minute. 每个程序都使用这种那种的资源--内存缓存,屏幕空间(screen space),网络连接,数据库资源等待。事实上,在面向
对象环境中,每个对象都标识你程序中某些可用的资源。如果要使用这些资源则要求分配到内存中来表示这些资源。访问这些资源的步骤如下:

 

    1. 在内存中分配表示该类型的资源
    2. 设置资源的初始化状态和使资源可访问来初始化内存
    3. 访问该类型的示例成员来使用这些资源(有必要则进行重复)
    4. 标识资源的状态为清除状态
    5. 释放内存

 

这个看起来简单的例子曾经是程序错误的主要来源。毕竟,有多少次你忘记释放不在使用的内存或者尝试去使用已经释放的内存?

 

这两个bug比任何其它程序都严重因为他们会导致什么样的结果和什么时候发生都是不可预测的。对于其它的bug,当你看到程序运行
不正确,你只要修复它就可以了。但这两个bug会导致资源溢出(内存消耗)和object corruption(不稳定性),使你的程序在不可预测
的时候发生不可预料的事情。事实上,有很多工具(如任务管理器,System Monitor ActiveX? Control, CompuWare's BoundsChecker, 和 Rational's Purify)用于帮助开发者来定位这些bugs。

 

在查看GC中,你会发现GC完成将开发人员从内存跟踪和何时释放内存中独立开来。然而,GC并不知道内存中用类型表示的各种资源。这意味着
GC无法执行上面的第4个步骤--改版资源的状态。要使资源正确的释放,程序员必须编写代码来正确的清除资源。在.net framework里面,开发者
需要编写类似Close,Dispose或者Finalize的方法,我们会在接下来介绍它。但是,接下来你会看到,GC会自动决定何时去调用这些方法。

 

同样的,很多类型标识的资源不需要做任何清除。比如,一个四方形资源可以通过释放该类型在内存中left,right,width和height字段就可以完全
释放资源。另外,一些表示文件的资源或者网络连接资源则要求执行特定的清除代码来释放资源。之后我将会介绍如何正确的完成这些工作。
现在我们要检测内存是如何分配的已经资源是如何初始化的。

 

资源分配

.net通用语言运行时要求所有的资源分配到托管堆中。这是一个类似C运行时堆除了你不需要释放托管堆的对象--当对象不再被程序使用时会自动被释放
这样也导致了一个问题:托管堆是如何知道对象不再被程序使用的?很快我就会解答这个问题。

 

现在有好几个GC算法。每个算法都是针对特定环境精心设计的以保证最好的性能。本来要集中讨论通用语言运行时的GC算法。我们来先看看基本的概念。

当程序初始化的时候,运行时会分配一段未被使用的内存地址区域。这段地址区域就是托管堆。这个堆同时维护这一个支持,我们就叫它“NextObjPtr”。
这个指针标识了在托管堆中下个对象将会被存储的地址。初始化的时候,NextObjPtr会被设置到该内存地址区域的基地址。

 

程序通过一个新操作创建一个对象。这个操作首先会确定该新对象占用的字节数是否能够在以分配区域得到满足(如果需要则申请更多内存)。
对过对象满足,则NextObjPtr指针在托管对中指向了该对象,该对象的构造函数被调用,然后这个新的操作会返回改对象地址。

这时,NextObjPtr会增涨改对象的大小,然后指向托管堆中的下一个对象。Figure 1可以看到三个连续的对象:A,B和C。下个对象将会被分配到NextObjPtr指针的位置(直接跟在C后面)

 

现在我们看看C运行时堆是如何分配内存的。在C运行时堆中,为一个对象分配内存需要遍历一个链表的数据结构。一定发现较大块的内存区域,该内存块就会被分割,然后链表中的所有指针就会修改以保持链表的完整性。对于托管堆,分配一个对象只是简单的把值赋给一个指针--两者相比这显得相当高效。事实上,从托管堆分配一个对象跟线程栈中分配内存是一样快的。

 

现在看来托管堆从速度和简单的实现比C运行时堆有更高的优越性。当然,托管堆 有这些优越性是因为它需要一个大前提:地址空间和危险的存储空间。这个前提(毫无疑问)是可笑的,因此托管堆必须提供某种机制来保证堆具有这个前提。这个机制叫做:GC,我们来看看它是如何运行的。

 

当程序使用一个操作创建一个对象,可能没有足够的内存区域来储存该对象。堆发现这个问题后则增加NextObjPtr的大小。如果NextObjPtr是在内存区域的结尾,则堆就满了同时必须启动回收。

 

事实上,回收会发生在generation 0满的时候。简单的说,generation就是GC引入的一种提高性能的机制。主要原理是新创建的对象被认为是年轻的一代,而在应用程序生命周期内早期创建的对象则被认为是老一代。将对象分为不同的代可以是GC回收特定的代,而不是回收整个托管堆。代的概念会在Part 2中更详细的介绍。

 

GC算法

GC回收会检查堆中是否存在对象是不被程序使用的。如果这样的对象存在,则这些对象使用的内存可以重新分配(如果堆没有足够内存,则会抛出OutOfMemoryException异常)。那么GC是如何知道对象是否被应用程序使用呢?你可能会意识到,这不是一个简单的问题。

 

每个程序都有一个根集合,根指向了托管堆中的对象或者被设置为null的对象存储的位置。例如,程序中所有的全局和静态对象都属于程序的根集合。
同样,所有在线程栈中局部变量/参数对象指针也属于程序的根。最后,任何包含指向托管堆对象指针的CPU registers也是程序根的一部分。
这个活动的根由JIT编译器和通用语言运行时来维护,同时使GC算法可访问。

 

当GC开始运行时,它会假设所有托管堆的对象都是垃圾。换句话说,它假设根集合并不指向托管堆中的任何对象。现在,GC开始从根开始遍历,同时
构建一个有所有可到达组成的图。例如,GC会从一个指向托管堆对象的全局变量开始。

 

Figure 2表示堆中有多个分配的对象,程序的根集指向了堆中的A,C,D和F对象。所有这些对象形成了一个图。当添加对象D时候,collector发现了对象
引用了对象H,因此对象H也被添加到图中。collector接着继续查找所有可以到达的对象。

 

一旦这部分的图完成,GC会接下来检测下一个根同时再次继续遍历对象。在GC一个个对象遍历的时候,如果试着添加一个之前添加过的新对象,GC就会停止
继续遍历那个路径。这有两个目的,第一,它能够显著的提高性能,因为它避免多次遍历同一路径。第二个目的是,如果你的对象有任何循环链表它可以避免死循环的发生。

 

一旦所有的根节点遍历完后,GC图中包含了所有从程序集根开始可以到达对象的集合。其它不在图中的对象则说明程序不可访问,所以可以认为是垃圾。
这时候GC继续遍历堆,查找是否存在连续的垃圾对象块(现在认为是自由空间)。GC则会把非垃圾对象移动到这些内存中(使用你已经知道好多年的memcpy函数)
移除堆中所有空白。当然移动内存对象会影响所有的对象指针。因此GC必须修改程序的根集,使得他们指向了对象的新地址。同样,如果对象包含了指向
另一个对象的指针,GC也需要修改这些指针。figure 3 表示回收资源后的情况。

所有垃圾被清除后,非垃圾资源被压缩起来,所有的非垃圾资源的指针也被修复好,NextObjPtr有重新定位到最后非垃圾对象的后面。这时,新对象创建时
资源的请求就可以很轻易的成功创建。

 

你可以发现,GC出现了一个明显性能损耗,这是使用托管堆的一个主要缺点。然后,记住GC只是在托管堆满的时候才会调用,其余时间托管堆的性能是比C运行时堆
快很多的。运行时的GC还提供了一些优化措施来提高GC的性能。我会在本文Part 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++写的。原因是GC必须能够标识程序的根集合同时必须能够找出所有对象指针。问题在于C++允许建立一个从一种类型指向另一类型的指针,而无法知道是指针指向哪个类型。在通用运行时,托管堆经常需要知道对象是属于哪个类型,元数据信息可以用于判断哪个对象引用了哪个对象。

 

Finalization

GC提供了一个附加的功能,可能让你更好的使用:finalization. Finalization运行一个资源在被回收的时候平和的回收。通过使用Finalize,一些表示
文件或者网络连接的资源可以GC决定释放资源内存时适当的清除自己。

 

这里有个简单的例子来说明上述的情况:当GC决定开始回收垃圾对象是,GC会首先调用对象的Finalize方法(如果存在的话)然后该对象资源就会被重置。
例如:我们来看看以下代码(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();

 

将来某个时候,GC会认为该对象是垃圾对象。这是会发生什么,GC会看到该对象有Finalize方法,然后就调用该方法,从而导致在控制台
输出“In Finalize”同时也回收该对象使用的内存块。

 

很多C++开发人员会马上会把析构函数和Finalize函数联系起来,那么我想告诫你的是:对象的finalization和析构函数具有不同的意义
在使用Finalize函数时你最好忘记一些你关于析构函数的内容。管理对象永远不会有析构函数--现在来说。

当设计一个对象时最好避免使用Finalize方法,因为有一下几个原因:

 

  • 具有Finalize方法的对象会使对象变成老一代,从而提示内存的压力和阻碍GC回收垃圾时决定该对象是否是垃圾。同时,所有直接或间接
    相关的对象也会变成老一代。我们会在Part 2讨论“代”以及它的升级。
  • Finalize对象分配内存时需要更多时间。
  • 强制GC去执行Finalize方法会显著的影响性能。记住,每个对象都会被Finalize。如果我有一个1万个对象的数组,则每个对象都必须调用Finalize方法。
  • Finalizable对象会影响到其它对象(非finalizable),延长它们不毕业的生命周期。事实上,你可以考虑将一个类分割成两个不同的类。
    其中一个带着Finalize方法不引用其它类型的轻量对象和一个没有Finalize方法,但是引用了其它对象的类。
  • 你无法控制何时执行Finalize方法。对象会占用资源直到下一次运行GC。
  • 当程序终结的时候,一些对象仍然是可到达的同时他们也没有Finalize方法。这种情况有 可能发生在后台进程正在使用对象或者对象在应用程序
    关闭或AppDomain卸载的时候创建。同时,默认情况下在程序关闭的时候那些不可达的对象是不会调用Finalize方法的,以保证程序可以快速的退出。
    当然,所有操作系统资源会被重新回收,当时对于托管堆的资源却不能适当的清除。不过你可以通过System.GC中的RequestFinalizeOnShutdown方法来改变这种默认情况。但是,在调用该方法的时候你必须格外小心,因为它可以控制整个应用程序。
  • 运行时不会保证Finalize方法的调用顺序。例如,如果有一个对象包含指向一个内部对象的指针。同时GC判断这两个对象都是垃圾对想,在加入内部的对象
    的Finalize方法会被首先调用。现在,外部对象的Finalize方法运行访问内部对象,并且调用它的方法,此时内部对象已经被回收了,所以结果是不可预料的。
    对于这个问题,我们强烈建议Finalize方法不要访问任何内部对象的方法。

如果你决定要引入Finalize方法,请保证这些代码执行速度尽量快。避免任何可以阻止Finalize方法调用的动作,避免在方法中包含异步线程操作。
同时,你如果使任何异常跳过Finalize方法,系统会调用Finalize方法且继续执行其它对象的Finalize方法。

 

当编译器生成代码时,在构造函数中,编译器会子哦的那个插入一个对基类构造函数的调用。同样的,当C++编译器生成代码时,在析构函数中,
编译器也会在析构函数插入对基类的析构函数的调用。然而,我之前说过,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#不支持析构函数。不要给这个相同的语句混淆了。

 

Finalization 内窥

表面上看,Finalization很直观:你创建 一个对象,当对象被回收时,Finalize方法就被调用。但是Finalization不仅如此。

 

当一个程序创建一个对象,实例化操作在托管堆中分配内存。如果对象具有Finalize方法,则指向该对象的指针被放在一个finalization队列
finalization对象是一个由GC控制的内部数据结构。队列中的每个指针指向的对象都必须在内存回收的时候先调用Finalize方法。

 

Figure 5可以看到堆中包含了几个对象。有些是可以从程序的根到达的,有些则不可以。当对象C,E,F,I和J创建时,系统洁厕到这些对象具有Finalize方法
于是指向这些对象的指针就被放到finalization队列中。

当GC运行的时候,对象B,E,G,H,I和J被检测为垃圾资源。GC首先会遍历finalization队列是否有指针指向这些对象。如果发现一个指针,该指针从finalization
队列移除,同时附加到freachable队列(读F-reachable)。同样freachable队列也是GC内部管理的一个数据结构。freachable队列中的每个对象标识这些对象
已经准备好可以调用finalize方法。

 

回收完成后,托管堆的内存情况如Figure 6。这里你可以看到B,G,H暂用的内存已经被释放,因为这些对象没有需要调用的Finalize方法。然而,E,I和J对象确
未能释放,因为它们的Finalize方法还未被调用。

这里有一个特殊的运行时线程专门用于调用Finalize方法。当freachable队列是空(通常是这样)的时候,则该线程为睡眠状态。当出现对象时,该线程被唤醒。
移除队列中的每个实体,同时调用对象的Finalize方法。正因为这个原因,你应该不要在Finalize方法中执行其它代码使得你的线程具有其它职责。
例如,避免在Finalize方法访问线程本地资源。

 

简单的说,如果一个对象是不在reachable的,那么GC认为它就是垃圾对象。然后GC会将对象从finalization队列移动到freachable队列,对象不再被认为是垃圾
同时也内存也不会被释放。GC会压缩已经回收的内存资源同时启动一个特殊的运行时线程来清空freachable队列,执行每个对象的Finalize方法。

下一次GC再次调用时候,可以看到finalize对象就真正成为垃圾了,因为程序的根集并不指向他们而且freachable队列也没有指向他们的指针。现在这些内存就会
被简单的回收。这里最重要的问题是finalization对象回收内存时,需要两次调用GC才能回收。事实上,还有可能不止是两次回收,因为这些对象可能变成更老的一代了。
Figure 7 可以看出第二次调用GC后的托管堆情况。

 

重生(Resurrection)

从整篇文章可以看到finalization挺好的,但是,它还不只是我描述的那么多。你可以发现在前面的章节中,当应用程序不在访问一个存活的对象,那么GC认为对象是死了。但那是,当对象具有Finalize方法时,对象又被认为是活过来了直到它的finalized方法被调用。这是一个很有趣的现象,我们叫:重生。重生,就像它名字说的,运行一个对象从消亡状态中回调。

 

我刚刚已经介绍重生的过程。当GC将一个对象的引用放到了freachable队列中,对象则可以从根集中到达,因此对象重生了。最后,对象的Finalize方法被调用,没有跟指针指向该对象,则对象永远的消亡了。但是,如果对象的Finalize方法调用了一个这些全局或静态变量时会发生什么情况呢?

 

public class BaseObj {
    
protected override void Finalize() {
        Application.ObjHolder 
= this
    }
}
class Application {
    
static public Object ObjHolder;    // Defaults to null
···
}

 

 

这种情况下,当对象的Finalize执行的时候,指向对象的指针位于根集合同时应用程序也可以到达这些对象。因此该对象又重生了,GC不再认为该对象是垃圾资源。
应用程序可以自由的使用该资源,但是有一个重要的问题需要注意的是,该对象已经被finalized过了,再次使用该对象可能会导致不可预测的结果。
同时需要注意:如果BaseObj包含指向其它对象的指针(无论直接或间接),所有对象都会重生,因为它们都可以从程序中的根集中访问到。但是,同时也需要注意
这些其它对象也可能已经finalized了。

 

事实上,当你设置一个类是,该类的对象的消亡和重生完全不在你的控制下的。你可以通过实现相关代码来处理这些问题。对于大多类型,这要求使用一个bool标识
来标识该对象是否已经消亡。接着,如果有方法调用已经消亡的对象,你就可以考虑抛出一个异常。该方法主要根据你的类型需要来实现。

陷入,如果有代码将Application.ObjHolder设置为null,那么对象就不可达。最终GC会认为该对象是垃圾对象,同时回收该对象的存储控件。注意,该对象的Finalize也不会被调用,因为Finalize队列中没有指向对象的指针。

 

很少情况下可以很好的利用重生,你应该尽量避免出现这种情况。但是,当人们使用重生一般情况下是希望对象消亡的时候能够平和的清除对象。为了实现该功能,GC类提供了一个ReRegisterForFinalize的方法,只要带一个指向对象指针的参数。

 

public class BaseObj {
    
protected override void Finalize() {
        Application.ObjHolder 
= this
        GC.ReRegisterForFinalize(
this);
    }
}

 

 

当对象的Finalize方法调用时,它会通过将根集的指针指向该对象来实现出哦你告诉。接着会调用ReRegisterForFinalize方法,将特定对象的指针存放到finalization队列当GC检测到对象不可达,它会将对象指针指向freachable低劣,同时Finalize方法会被再次调用。上面代码展示了如何创建一个自我重生的对象且永远不会消亡,虽然这通常是不推荐的做法。比较常用的做法是有条件的在Finalize方法中设置根集的引用。

 

你必须确保每次重生不多次调用ReRegisterForFinalize方法,或者多次调用Finalize方法。因为每次调用ReRegisterForFinalize方法都会在finalization队列末尾
添加一个新的实体。当对象被认为是垃圾时,所有的这些实体会被移动到freachable队列,同时多次的调用Finalize方法。

 

强制清除对象

如果可能,尽量设置不需要任何清除操作的对象。不幸的是,对于很多类型,这并不太可能。对于这些类型,你必须在类型定义的时候引入Finalize方法。
然而,我们建议添加一个方法可以允许用户在他们需要的时候明确的清除对象。通常,我们将该方法命名为:Close或Dispose。

 

通常,如果对象在关闭之后可以重新打开或者重用你可以使用Close方法。你可可以使用Close方法来标识对象认为是关闭的,比如文件。另一方面,
你可以使用Dispose方法来表示对象在Dispose后不能再被使用。例如,当你要删除一个System.Drawing.Brush对象,你应用调用它的Dispose方法。
一定Dispose,该Brush对象就不能再被使用,如果再调用该对象的方法会导致抛出异常。如果你还需要其它Brush对象,那么必须重新实例化一个Brush对象。

现在我们来看看Close/Dispose应该做什么。System.IO.FileStream对象允许用户打开一个文件进行读和写。为了提高性能,该类型使用了内存缓冲区。只有在
缓冲区满了之后,缓存区才会把内容存放到文件中。假设当你创建一个FileStream对象,而只写了少量的字节信息。如果这些信息没有填满缓冲区,那么缓冲区
不会写到文件去。FileStream引入了Finalize方法,当FileStream对象被回收的时候,Finalize方法会把所有数据从内存中移到文件,并且关闭文件。

 

但是这样的方法来使用FileStream并不是很好。加入FileStream还没回收,但是应用程序有需要传进一个新的FileStream对象。在这种情况下,如果第一个
FileStream对象仍打开文件,那么第二个FileStream会因为拒绝访问而打不开文件。FileStream的用户需要某种方法可以强制将最终的内存清空到硬盘中,然后关闭文件。

 

如果你查看FileStream的文档,你会发现它包含了Close方法。当调用它的时候,该方法会将内存的中的剩余数据清空到文件中然后关闭文件。此时其它FileStream对象就可以使用了。

 

但是现在又出现了一个有趣的问题:FileStream对象回收的时候,它的Finalize方法究竟做什么呢?显然,答案是不做任何事情。事实上,如果我们明确的调用了Close方法,我们没理由让FileStream的Finalize执行,你知道我们不建议使用Finalize方法,在这个场景中,你应该让系统去执行一个不做任何事情的Finalize方法。
现在看上去有必要通过某种方法来禁止系统调用Finalize方法。幸运的是,System.GC对象就有这样的一个静态方法SuppressFinalize,通过一个简单参数,传递对象的指针来实现。

 

Figure 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
···
}

 

 

我们来测试一个相关的问题,下面是经常用到的通过StreamWriter传入FileStream对象

 

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的指针。这两个对象内部都有缓冲区在
你完成访问文件的时候都需要清空到文件中。调用StreamWriter的Close方法可以将最终的数据写到FileStream,然后在内部调用FileStream的Close方法,
最终将数据写到文件中并关闭文件。因为StreamWriter的Close方法同时也关闭了相关连的FileStream对象,你不必再自己调用fs.Close。

 

如果移除这两个Close的调用,你觉得会发送什么事呢?好吧,当GC会正确的觉得该对象是垃圾也会将对象清除。但是,GC不会理会那个对象的Finalize方法
会被首先调用。所以,如果FileStream的finalize方法被调用,它就关闭文件。接着当StreamWriter的Finalize被调用是,它会尝试去访问关闭的文件,
导致出现异常。当然,如果StreamWriter首先被调用,那么数据可以安全的写回文件中。

 

那么微软是如何解决这个问题的?让GC按照一定的顺序去回收资源是不可能的,因为对象包含了指向其它对象的指针,同时GC也没办法正确的猜测回收这些资源的顺序。因此,下面是微软的解决方案:StreamWriter不会实现Finalize方法。当然,这意味着如果你不明确的调用Closee方法,那么你的数据将会丢失。微软希望开发人员能够发现丢失数据同时在代码中调用close方法来解决这个问题。

 

就像早前说的,SupressFinalize方法会设置一个标识来表示对象的Finalize方法不能被调用。但是,当运行时决定是时候调用Finalize方法时,该标识会被重置。
这意为着通过调用SuppressFinalize不可以平衡ReRegisterForFinalize的调用。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.
}

 

 

ReRegisterForFinalize和SuppressFinalize的引入是为了性能问题。只要每次调用了SuppressFinalize方法,那么ReRegisterForFinalize也会被关联调用。
你必须确保不重复调用ReRegisterForFinalize或SuppressFinalize,或者重复调用对象的Finalize方法也会导致这种情况。

 

总结

 

GC环境的动机是为了方便开发者对内存的管理。这部分的内容是对GC的概念和内部做一个大概的了解。在第二部分,我会结束这个讨论。
首先,我会介绍调用WeakReferences的功能,你可以使用它来减轻托管堆中大对象内存的压力。紧接着,我会测试一个手工扩展托管对象的
生命周期的机制。最后,我会总结的讨论一些影响GC性能的问题。我会讨论代,多线程回收,CLR公开的性能计数器,它可以用来就爱内测GC的真实动作。

 

原文下载

 

posted @ 2010-08-27 14:31  Chris Cheung  阅读(1484)  评论(0编辑  收藏  举报