为祖国健康工作50年

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

第八章 对象的生命周期

本章主要介绍了CLR怎样通过垃圾回收来管理已分配的对象。.NET对象被分配到一块叫做托管堆的内存区域上,在那里它们会在将来的某个时刻被垃圾回收器(以下简称GC)自动销毁。之后了解了System.GC类型通过编程使用垃圾回收器。接着分析了System.Object.Finalize()虚方法和IDisposable接口建立及时释放内部非托管资源的类型。

8.1 类、对象和引用

类是一个蓝图,描述了这个类型的实例(对象)在内存中是什么样子,类是定义在一个代码文件中的。

对象是在定义一个类后,就可以使用C#的new关键字分配任意数量的对象,在托管堆中为其分配内存。但是,new关键字返回的是指向堆上对象的引用,而不是真正对象本身。这个引用变量保存在栈中,以供应用程序使用。因此,从整体上看,一个对象由在栈中的引用和在托管堆中的对象实体构成。

8.2 内存管理法则

在创建c#应用程序时,无需对托管堆进行直接操作,它将自行管理。.NET进行内存管理的法则主要有四条,下面章节中分别介绍。

(1) 法则1:使用new关键字将一个对象分配到托管堆中,然后不用再管。

(2) 如果托管堆没有足够内存来分配所请求的对象,就会进行垃圾回收。

(3) 重写Finalize唯一的原因是,c#类通过pinvoke或者复杂的COM互操作性任务使用了非托管资源。

(4) 如果对象支持IDisposable,总是要对任何直接创建的对象调用Dispose(),应该认为,如果类设计者选择支持Dispose方法,这个类型就需要执行清理工作。

8.3对象声明周期基础(法则1)

根据法则1,实例化结束之后,GC将会在对象不再需要时将其销毁。如何判断一个在托管堆中对象不再需要呢?简单说,就是一个对象从代码库中任何部分都不可达时,就会在垃圾回收时将其从堆中删除。例如,对于一个方法中定义的一个对象,那么并且这个对象没有被传递到方法定义作用域之外,则当方法调用结束后,这个对象的引用就不在可达,相关联这个对象就是垃圾回收的候选目标。但是,不能保证他成为候选回收目标后就立刻被回收,可以肯定的是,当CLR进行下一次垃圾回收时,将被完全的销毁。

疑问:“可以肯定的是,当CLR进行下一次垃圾回收时,将被完全的销毁。”,谁知道是不是仅仅是被标记为下一代还是真的就回收了呢??

自动垃圾回收大大简化应用程序开发,避免了内存泄露(未及时释放内存而导致内存无法再试试用,导致浪费)的问题。

New关键字在CLR中对应一条newobj指令,为了辅助托管堆进行垃圾回收,托管堆保存了一个指针(称为下一个对象指针或新对象指针),它精确指示下一个对象将被分配的位置。

在newobj发出指令后,CLR将执行下列核心任务:

1)计算分配对象所需内存总数(包括成员变量和类型的基类的必须内存)。

2)检查托管堆,确保有足够空间来访只要分配的对象,如果有,则调用类型的构造函数,在托管堆中为其分配空间,并将当前的下一个对象指针作为这个对象的引用返回给调用者,然后移动下一个对象的指针,指向托管堆下一个可用的位置;若空间不够,则根据法则2,它会实行一次垃圾回收来尝试释放内存,释放后有了新的下一个对象的指针,然后按照上面有足够空间的逻辑进行分配。

8.4垃圾回收过程(法则2)

当确实发生回收时,GC挂起当前进程中所有活动的线程,以保证应用程序在回收过程中不会访问堆。一旦回收周期完成,就允许挂起线程继续他们的工作。由于GC进行了精心优化,因此用户很少察觉到程序中断。

已经说了,垃圾回收将会在将来的某个时间发生,而这个过程往往是不确定的(除非强制垃圾回收),因此,一些程序员按照之前c++的方法讲一个不再使用的对象设置为null,认为这就可以被立刻回收而释放内存了,这种想法是不对的,在c#中,将引用赋值为null并不意味强制GC立刻启动并把对象从堆中移除,它仅仅是显示取消了引用和之前引用所指向对象之间的连接(我认为仅仅是你无法再访问这个对象了,因为引用为空了,但是这个对象还是真实存在的,另外,栈中的引用由于设置为null,则立刻弹出栈,成为可用区域!)。因此,设置null的意义并不大,但是也没什么害处。

在垃圾回收启动时时,依赖于两个重要概念:应用程序根和对象的代。

应用程序根:前面说过一个对象不可达时候就是可以作为垃圾回收的备选了,但是如何确定不可达呢,这就需要应用程序跟,根就是一个存储位置,保存着对堆上一个对象的引用。

垃圾回收时,首先运行库将检查托管堆中的对象(和他们可能包含的任何内部对象引用),判断应用程序是否仍然可达到他们,也就是是否有根或者说是否有活动根。这个根的生成是根据对象图生成的。对象图包含堆上可达的每一个对象,而且不会让同一个对象在对象图中出现一次以上。一旦生成了对象图,不可达的对象就被标记为垃圾。标记为终结后,他们就会从内存中清除(不一定都清除哦,空间释放够了就可能暂时不清除,这在下面对象的代中可以解释),剩余空间被压缩(称为重新定位),继而引起CLR修改活动应用程序根的集合,指向正确的内存位置,最后,下一个对象的指针被重新调整以指向下一个可用位置。

实际上,GC使用两个不同的堆,一个专门用于存储非常大的对象,这个堆得垃圾回收中较少顾及,因为重新定位的开销很大。

对象的代:上面说了,在垃圾回收时CLR会检查对象是否不可达对象,但是并不是检查托管堆中每一个对象,显然,全部检查将消耗大量时间。为了优化这个过程,堆上的每个对象被指定为属于某“代”,其设计思路就是:对象在堆中存在时间越长,就更可能应该保留。每个堆的对象属于下列某代:

第0代:从没有被标记为回收的新分配的对象;

第1代:在上一次垃圾回收中没有被回收的对象(也就是,他被标记为回收了,但是因为已经获取了足够的空间,而暂时没有被删除);

第2代:在上一次以上的垃圾回收后仍然没有被回收的对象。

在检查对象时,首先检查第0代,如果标记和清除这些对象后得到所需数量空闲内存,任何没有被回收的对象被提升为第1代。如果算上第0代上所有对象后仍然需要更多内存,就会检查第1代对象的“可达性”,并相应的进行回收。没有被回收的第1代对象随后被提升到第2代。如果仍然需要更多内存,就会检查第2代对象可达性,这时,如果一个第2代对象在垃圾回收后仍然存在,它仍然是第2代对象,因为这是对象代的上限。

疑问:“如果算上第0代上所有对象后仍然需要更多内存”,干嘛要算上所有对象内存而不是剩余的第0代空闲空间呢?

疑问:若第2代回收后还是不够呢?

这里的要点是,通过给堆上对象赋一个表示代的值(当然是系统来赋值,他会根据这个对象的类型和作用域等情况进行分配),尽快的删除一些较新的对象(比如本地变量),而不会经常打扰一些旧的对象(例如应用程序对象)。也就是一个对象有可能刚刚分配就被放在第2代,这是可以的,不是必须从0代开始依次分配。

.NET平台提供了一个名为System.GC的类类型,他通过编程使用一个静态成员集合与GC进行交互,方便查看托管堆的情况和垃圾回收信息。例如统计某一代进行了多少次垃圾回收、一个对象当前的代、顾及分配给的托管堆的内存数量、系统支持的代的数量等。其中一个最为重要的是GC.Collect方法,在一些非常罕见的情况下,通过编程用此方法强制进行垃圾回收可能会有好处,就是:

应用程序将要进入一段代码,后者不希望被可能得垃圾回收中断;

应用程序刚刚分配非常多的对象,想尽可能多的删除已获得的内存。

此时需要显式的触发一次垃圾回收,在手动强制回收时,总是调用GC.WaitForPendingFinalizers(),这样,就可以稍等片刻,已确定在程序继续执行之前,所有可终结的对象必须执行所有必要的清理工作。在底层,WaitForPendingFinalizers会在回收过程中挂起调用的线程,这是件好事,保证代码不调用当前正在被销毁的对象的方法。

GC.Collect();/*也可以加入第一个数值参数,指定是对某一个代进行强制回收,也可加入第二个参数,用于调整运行库如何强制进行垃圾回收*/

GC.WaitForPendingFinalizers();

疑问:不调用GC.WaitForPendingFinalizers()会怎么样呢?

需要注意的是,即使只做了一次垃圾回收的显示请求,CLR在幕后也执行了多次垃圾回收。

8.5构建可终结的对象(法则3)

在第六章中,知道了Object类有一个Finalize方法,这个默认实现是什么都不做,这个方法是保护级别,因此不可能通过点运算符从类实例(对象)直接调用一个对象的Finalize方法,也就是不能手动的直接调用。相反,只能自动发生于一次自然的垃圾回收或者通过GC.Collect强制回收的过程中,或者当成在应用程序的应用程序域从内存卸载时的过程中,在从内存删除这个对象时,GC会自动调用这个方法(如果支持这个方法的话)。也就是CLR自动调用在它的生命周期中创建的每一个可终结对象的终结器(析构函数)。

另外,对值类型的Finalize重写是不合法的,因为值类型本身就不在托管堆,无法被垃圾回收。

大多数C#类不需要显式清理逻辑,也不需要自定义终结器(析构函数),因为如果使用的是托管对象,一切最终会被回收。只是在使用非托管资源时,才可能需要设计一个用完后清理自身的类。这就是第三条原则。

法则3清理非托管资源,需要重写System.Object.Finalize()方法,以便在垃圾回收时,被自动调用。但是,比较奇怪的是,这个重写不再使用override关键字(protected override void finalize),这会导致编译错误。C#提供了析构函数语法来达到同样效果。析构函数和构造函数类似,必须和类同名,但是不能有参数,不能有返回类型,只能一个类有一个析构函数,形式如下(注意波折号):
~myresourcewarpper()

{

//清理这里非托管资源

}

实际上,CLR在IL中间语言中,还是会将其转换为Finalize方法,而且加入了必要的错误检测代码(try/catch)。

一些细节:应该牢记Finalize方法的作用是保证.NET能在垃圾回收时清除非托管资源,如果创建了一个不使用非托管实体的类型,终结是没有用的。事实上,如果可能,应该避免在设计类时提供Finalize方法(析构函数),因为终结要花费时间。

整个过程是:在托管堆中分配对象时,运行库自动确定该对象是否提供了自定义的Finalize方法,如果是,对象被标记为可终结,同时一个指向这个对象的指针被保存在名为终结队列的内部队列中,终结队列是由GC维护的表,它指向每个再从堆上删除之前必须被终结的对象。

当GC确定到了从内存释放一个对象时,他检查终结队列上每项,并将对象从堆上复制到另一个称作终结可达表的托管结构中,此时,下一个垃圾回收时将产生另外一个线程,为每一个在可达表中的对象首先调用Finalize方法,因此,为了真正终结一个对象,至少需要两次垃圾回收。

总之,尽管对象的终结能够保证对象可以清除非托管资源,但是本质上仍然是非确定的,而且由于额外的幕后处理,速度变得相当慢。

疑问:“如果创建了一个不使用非托管实体的类型,终结是没有用的”不太理解?

8.6构建可处置的对象(法则4)

上面说了,为了在垃圾回收时清理非托管资源,可以利用终结器。但是,因为很多非托管资源是很宝贵的,所以应该尽可能快的被清除,而不是依靠垃圾回收的发生。除了重写Finalize,类还可以实现IDisposable接口。如果提供了这个接口,就是假设当对象的用户不再使用这个对象时,会在这个引用离开作用域之前手动的调用Dispose方法。这样,就可以执行任何必要的非托管资源清理,而且不会再有将对象放在终结队列上导致的性能损失,而且不必等待垃圾GC触发类的终结逻辑。这就是法则4。

注意,值类型和引用类型都可以调用Dispose方法,因为二者都可以实现IDisposable接口。

Public class myresourcewarpper:IDisposable

{

Public void Dispose()

{

//清除非托管资源

//抛弃包含的其他可处置对象

}

}

Dispose方法不只是负责释放一个对象非托管资源,还应该对任何它包含的可处置对象调用Dispose方法。与Finalize不同,Dispose方法与其他托管对象的通信时安全的,因为GC并不支持IDisposable接口,因此永远不会自动调用Dispose方法,显然,Dispose方法会在GC之前的某个手工调用时候发生,不会担心要处置的资源已经被回收而出错。但是这也有个弊端,就是实现了Dispose方法来释放非托管资源,但是如果使用时忘记手动调用,那么非托管资源可能永远存于内存当中。

判断一个类型是否支持IDisposable接口,可以查看SDK,也可以通过is或as关键字判断是否支持此接口。

另外,和Finalize类似,积累库中许多都实现了IDisposable的类,还提供了Dispose方法的别名,例如fs.colse()等价于fs.dispose(),但是,调用后者总是正确的。

为了保证手动调用Dispose方法而把每个可处置的类型显式的包装在try/catch块中,显然很麻烦,因此,C#提供了特殊语法,利用using关键字,来自动调用Dispose方法,并在IL中扩展了Dispose方法,加入了try/catch逻辑。

Using(myresourcewarpper rw=new myresourcewarpper())

{

//使用rw对象

}

这个语法结构保证在退出using块后,正在使用的对象(rw)将自动调用其Dispose方法。同时,也可以在using作用域中声明相同类型的多个对象(用逗号分开),编译器会插入代码来调用每一个对象的Dispose方法。

8.7构建可终结和可处置的类型

上面介绍了两种释放非托管资源的方式。一方面可以重写Finalize方法,在内存回收时被自动调用,另一方面,实现Dispose方法,让用户手动调用并释放非托管资源。两种技术各有特点,也有各自的弊端,将两种方式放入同一个类,是可行的好方法。

这样,如果用户记住调用了Dispose方法,那么可以通过GC.SuppressFinalize()方法来通知GC跳过终结过程(调用Finalize方法),如果用户忘记调用Dispose方法,那么最终对象也会被GC通过调用Finalize方法而释放。总之,对象的内部非托管资源会用其中一种方式释放掉。

正式的处置方式微软给出了一个有些刻板的处置模式(MyResourceWrapper版本),他在健壮性、可维护性和性能三者间取了平衡。它确保了Finalize方法不会尝试处置任何托管对象,而Dispose方法应该这样做,并且确保对象用户可以安全地多次调用Dispose方法而不出错,还将两个方法都需要调用的代码通过辅助私有函数实现代码的简练。

posted on 2010-06-18 10:42  lerit  阅读(2224)  评论(9编辑  收藏  举报