每一个应用程序都要使用这样或者那样的资源,好比文件、内存缓冲区、屏幕空间、网络链接、数据库资源等。事实上,在面向对象的环境中,每一个类型都表明可供程序使用的一种资源。
要使用这些资源,必须为表明资源的类型分配内存。
访问一个资源所需的具体步骤以下:
#1,调用IL指令newobj, 为表明资源的类型分配内存。C#中使用new操做符,编译器就会自动生成该指令。
#2,初始化内存,设置资源的初始状态,使资源可用。类型的实例构造器负责设置该初始状态。
#3,访问类型的成员(可根据须要反复)来使用资源。
#4,摧毁资源的状态以进行清理。
#5,释放内存。垃圾回收将独自负责这一步。
须要注意的是,值类型(含全部枚举类型)、集合类型、String、Attribute、Delegate和Exception 所表明的资源无需执行特殊的清理操做。如,只要销毁对象的内存中维护的字符数组,一个String资源就会被彻底清理。
CLR要求全部的资源都从托管堆(managed heap)分配。应用程序不须要的对象会被自动清除。那么“托管堆又是如何知道应用程序再也不用一个对象?”
进程初始化时,CLR要保留一块连续的地址空间,这个地址空间最初并无对象的物理内存空间。这个地址空间就是托管堆。托管堆还维护着一个指针,我把它称为NextObjPtr。指向下一个对象在堆中的分配位置。刚开始时候,NextObjPtr设为保留地址空间的基地址。
IL指令newobj用于建立一个对象。许多语言都提供了一个new操做符,它致使编译器在方法的IL代码中生成一个newobj指令。newobj指令将致使CLR执行以下步骤:
#1,计算类型(极其全部基类型)的字段须要的字节数。
#2,加上字段的开销所需的字节数。每一个对象都有两个开销字段:一个是类型对象指针,和一个同步块索引。
#3,CLR检查保留区域是否可以提供分配对象所需的字节数,若有必要就提交存储(commit storage)。若是托管堆有足够的可用空间,对象会被放入。对象是在NextObjPtr指针指向的地址放入的,而且为它分配的字节会被清零。接着,调用类型的实例构造器(为this参数传递NextObjPtr), IL指令newobj(或者C# new 操做符)将返回对象的地址。就在地址返回以前,NextObjPtr指针的值会加上对象占据的字节数,这样会获得一个新值,它就指向下一个对象放入托管堆时的地址。
做为对比,让咱们看一下C语言运行时堆如何分配内存,它为对象分配内存须要遍历一个由数据结构组成的链表,一旦发现一个足够大的块,那个块就会被拆分,同时修改链表节点中的指针,以确保链表的完整性。
对于托管堆,分配对象只需在一个指针上加一个值 - 这显然要快得多。事实上,从托管堆中分配对象的速度几乎能够与从线程栈分配内存媲美!
另外,大多数堆(C运行时堆)都是在他们找到可用空间的地方分配对象。因此,若是连续建立几个对象,这些对象极有可能被分散,中间相隔MB的地址空间。但在托管堆中,连续分配的对象能够确保它们在内存中是连续的。
托管堆彷佛在实现的简单性和速度方面远远优于普通的堆,如C运行时堆。而托管堆之因此有这些好处,是由于它作了一个至关大胆的假设 - 地址空间和存储是无限的。而这个假设显然是不成立的,也就是说托管堆必须经过某种机制来容许它作这样的假设。这个机制就是垃圾回收器。
垃圾回收的工做原理
CLR的垃圾回收(garbage collection)。
应用程序调用new操做符建立对象时,可能没有足够的地址空间来分配对象。托管堆将对象须要的字节数加到NextObjPtr指针中的地址上来检测这种状况。若是结果值超过了地址空间的末尾,代表托管堆已满,必须执行一次垃圾回收。
重要提示:
前面的描述有些过于简单,事实上,垃圾回收是在第0代满的时候发生的。有的垃圾回收器使用了代(generation)的机制,该机制惟一的目的就是提高性能。其基本思路,在应用程序的生存期中,新建的对象是新一代,而建立得比较早的是老一代。第0代就是最近分配的对象,从未被垃圾回收算法检查过。在一次垃圾回收中,存活下来的对象被提高到另外一代(如第1代)。将对象划分为代,使垃圾回收器能专一于回收特定的代,而不是每次都要回收托管堆中的对象。这里假设垃圾回收是在堆满的时候发生的。
垃圾回收器检查托管堆中是否有应用程序再也不使用的任何对象。若是有,它们使用的内存就能够回收(如果垃圾回收以后,堆中仍然没有可用的内存,new操做符将会抛出一个OutOfMemoryException)。垃圾回收器如何知道应用程序正在使用一个对象?这的确不是一个三言两语就能够说清楚的问题。
每一个应用程序都包含一组根(root)。每一个根都是一个存储位置,其中包含指向引用类型对象的一个指针。该指针要么引用托管堆中的一个对象,要么为null。
如,类型中定义的任何静态字段被认为是一个根。除此以外,任何方法参数或局部变量也被认为是一个根。只有引用类型的变量才被认为是根;值类型的变量永远不被认为是根。
垃圾回收器会检查寄存器中引用的对象都是根,而这些根引用的堆中的对象不该被视为垃圾。除此以外,垃圾回收器还能够沿着线程的调用栈上行,检查每一个方法的内部表来肯定全部调用方法的根。最后,垃圾回收器遍历全部类型对象来获取静态字段中存储的根集合。
垃圾回收器开始执行时,它假设堆中全部对象都是垃圾。换句话说,它假设线程栈中没有引用了堆中对象的变量,没有CPU寄存器引用队中的对象,也没有静态字段引用堆中的对象。
第一阶段 - 标记
垃圾回收器的第一个阶段就是所谓的
标记(marking)阶段。在这个阶段,垃圾回收器沿着线程栈上行以检查全部根。若是发现一个根引用了一个对象,就在对象的“同步块索引字段”上开启一位 (即设置一个bit,或者说设置为1)--对象就是这样“标记”的。若标记某一个对象时,发现这个对象(如D对象)含有一个引用了另外一个对象(如H对象)的字段,会形成这个H对象也被标记。垃圾回收器就是如此,以递归的方式遍历全部可达的对象。
标记好根和它的字段引用的对象以后,垃圾回收器检查下一个根,并继续标记对象。若是垃圾回收器试图标记一个先前标记过的对象,就会中止沿着这个路径走下去。这个行为有两个目的。一是,垃圾回收器不会屡次遍历一组对象,因此性能获得显著提升;而是,若是存在对象的循环链表,能够避免陷入无限循环。
检查好全部根以后,堆中将包含一组已标记和未标记的对象。已标记的对象是经过应用程序的代码可达的对象,而未标记的对象是不可达的。不可达的对象被认为是垃圾,它们占用的内存能够回收。
第二阶段 - 压缩
如今,垃圾回收器开始第二个阶段,即
压缩(compact)阶段,在这个阶段中,垃圾回收器线性地遍历堆,以寻找未标记(垃圾)对象的连续内存块。(注意:此处“压缩”并不是压缩,即托管堆,增多可用内存;相反,这里的压缩更接近于“碎片整理”,事实上,正确意思即“变得更加紧凑”。这个事实上,源于上世纪的80年开始,人们将compact当作是compress的近义词而翻译成“压缩”,以讹传讹至今。)
若是发现的内存块比较小,就忽略它们。可是,若是发现大的、可用的连续内存块,垃圾回收器会把非垃圾的对象移动到这里以压缩堆。
很天然,移动内存中的对象以后,包含"指向这些对象的指针"的变量和CPU寄存器如今都会变得无效。因此,垃圾回收器必须从新访问应用程序的全部根,并修改它们来指向对象的新内存位置。另外,若是对象中的字段指向的另外一个已经移动了位置的对象,垃圾回收器也要负责改正这些字段。堆内存压缩以后,托管堆的NextObjPtr指针将指向紧接在最后一个非垃圾对象以后的位置。以下所示,一次垃圾回收后的托管堆:
如你所见,垃圾回收会形成显著的性能损失,这是使用托管堆的主要特色。但要注意的是,垃圾回收只在第0代满的时候才会发生。在此以前,托管堆的性能远远高于C运行时堆。
最后,CLR的垃圾回收器提供了一些特殊的优化措施,能够大幅度提升垃圾回收的性能。
到这里,做为一名程序员,你应该从前面的论述得出四点重要的认识:
第一点,没必要本身实现代码来管理应用程序所用的对象的生存期。垃圾回收机制使开发人员获得解放,无需关注内存释放,能够专一真正要解决的问题
第二点,再也不发生对象泄露的状况,由于任何对象只要没有应用程序的根引用它,都会在某个时刻被垃圾回收器回收,因此应用程序将不可能再发生内存的泄露的状况。
第三点,应用程序也再也不可能访问一个被释放的对象。由于,假如对象可达,就不会被释放;假如不可达,应用程序就是没得办法访问它。
第四点,由于垃圾回收致使了内存的压缩(compact),因此托管对象不可能形成进程的虚拟地址空间的碎片化。若是是非托管堆,如C运行时堆,地址空间的碎片化现象可能很是严重。而后,一个例外是在使用大对象的时候,仍然是有可能碎片化的。
重要提示:在负责加载类型的那个AppDomain卸载以前,类型的静态字段永远是它引用的任何 对象的根,形成内存泄露的一个常见缘由就是让某个静态字段引用一个集合对象,而后不停地向集合对象添加数据项。静态字段保持集合对象的存活,而集合对象保持它的全部数据项的存活。有鉴于此,应该尽可能避免使用静态字段。
使用终结操做来释放本地资源
大多数类型只须要内存就能够正常工做,可是也有一些类型除了要使用内存,还要使用本地资源。
如,System.IO.FileStream类型须要打开一个文件(本地资源)并保存文件的句柄。而后,该类型的Read和Write方法用该句柄来操做文件。
终结(finalization)是CLR提供的一种机制,容许对象在垃圾回收器回收其内存
以前执行一些得体的清理工做。任何包装了本地资源(如文件,网络链接、套接字、互斥体或者其余类型)的类型都必须支持终结操做。
简单地说,类型实现了一个命名为Finalize的方法。当垃圾回收器判断一个对象是垃圾时,会调用对象的Finalize方法(若是有的话)。能够这样理解:实现了Finalize方法的任何类型其实是在说,它的全部对象都但愿在“被处决以前吃上最后一顿餐”。
Microsoft C#团队认为,Finalize方法是在编程语言中须要特殊语法的一种方法(相似于C#要求用特殊的语法定义构造器)。所以,在C#中,必须在类名前加一个~符号来定义Finalize方法,以下所示:
C#定义的Finalize方法的特殊语法很是相似于C++定义析构器的语法。事实上,在C#编程语言规范的早起版本中,真的是将该方法称为析构器。可是,Finalize方法的工做原理和非托管C++的析构器彻底不一样,这会使从一种语言迁移到另外一种语言的开发人员产生极大的混淆。
实现了Finalize方法时,通常都会调用Win32 CloseHandle函数,并向该函数传递本地资源的句柄。例如,FileStream类型定义了一个文件句柄字段,它标识了本地资源。
FileStream类型还定义了一个Finalize方法,它在内部调用CloseHandle函数,并向它传递文件句柄字段。这就确保了在托管的FileStream对象被肯定为垃圾后,本地文件句柄会得以关闭。如果包装了本地资源的类型没有定义Finalize方法,本地资源就得不到关闭,致使资源泄露,直至进程终止。进程终止时,这些本地资源才会被操做系统回收。
Finalize 方法是在垃圾回收器回收前调用,可是,CLR并不保证各个Finalize方法的调用顺序。
如下五种事件会致使垃圾回收:
#1,第0代满
第0代满时,垃圾回收会自动开始。该事件是目前致使Finalize方法被调用的最多见的一种方式,由于随着应用程序代码运行并分配新对象,这个事件会天然而然地发生。
#2,
代码显式调用System.GC的静态方法Collect
代码能够显式请求CLR执行垃圾回收,虽然Microsoft强烈建议不要这样作,但某些时候仍是有必要的。
#3,
Windows报告内存不足
CLR内部使用Win32的CreateMemoryResourceNotification和QueryMemoryResourceNotification 函数来监视系统的整体内存。若是Windows报告CLR内存不足,CLR将强制执行垃圾回收,即尝试释放已经死亡的对象,从而减少进程工做集的大小。
#4,
CLR卸载AppDomain
一个AppDomain被卸载时,CLR认为该AppDomain中再也不存在任何根,所以会对全部代码的对象执行垃圾回收。
#5,
CLR关闭
一个进程正常终止时(相对于从外部关闭,好比经过任务管理器关闭),CLR就会关闭。在关闭过程当中,CLR会认为该进程中不存在任何根,所以会调用托管堆中的全部对象的Finalize方法。注意,CLR此时不会尝试压缩或释放内存,由于整个进程都要终止,将由Windows负责回收进程的全部内存。
CLR使用一个特殊的,专用的线程来调用Finalize方法。对于前4种事件,若是一个Finalize方法进入了无限循环,这个特殊的线程会被阻塞(blocked),其余Finalize方法将得不到调用。这种状况很是糟糕,由于应用程序永远都不能回收由可终结的对象占据的内存 - 只要应用程序运行,就会一直泄露内存。
对于第5种事件,每一个Finalize方法有大约2秒钟的时间返回。若是Finalize方法在2秒钟内没有返回,CLR将直接杀死(结束)该进程 - 不会调用更多的Finalize方法。另外,若是调用全部对象的Finalize方法的时间超过了40秒钟(也许之后会改变这个值),CLR也会杀死进程。
补充介绍:
#1,若是AppDomain卸载,其AppDomain的IsFinalizingForUnload 方法将返回true。
#2,若是进程终止,System.Enviroment.HasShutdownStarted属性将返回true。
终结操做的秘密
终结操做彷佛很简单: 建立一个对象,当它被回收时,它的Finalize方法会获得调用。而若深究,你会发现远非这么简单。
应用程序建立一个新对象时候,new操做符会从堆中分配内存。若是对象的类型定义了Finalize方法,那么在该类型的实例构造器被调用以前,会将指向该对象的一个指针放到一个终结列表(finalization list)中。
终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每个项都指向一个对象 - 在回收该对象的内存以前,应该调用它的Finalize方法。
注意
System.Object定义了一个Finalize方法,虽然如此,可是CLR会忽略它。也就是说,构造一个类型的实例时,若是该类型的Finalize方法是从System.Object继承的,就不认为这个对象是“可终结”的。类型必须重写Object的Finalize方法,这个类型及其派生类型的对象才被认为是“可终结”的 。
如上图,垃圾回收开始,对象B, E, G, H, I 和J被断定为垃圾。垃圾回收器扫描终结列表以查找指向这些对象的指针。找到一个指针后,该指针会从终结列表中移除,并追加到freachable队列中。freachable队列(发音是“F-reachable”)是垃圾回收器的另外一个内部数据结构。它中的每一个指针都表明其Finalize方法已经准备好调用的一个对象。垃圾回收完毕后托管堆的状况:
能够看出,对象B, G和H占用的内存已经被回收,由于它们没有Finalize方法。可是,对象E, I和J占用的内存暂时不能回收,由于它们的Finalize方法尚未调用。
重要信息:
终结列表和freachable队列之间的交互很是有意思。
首先,让我告诉你freachable队列这个名称的由来。“f”明显表明“终结”(finalization);freachable队列中的每一个记录项都是对托管堆中的一个对象的引用,该对象的Finalize方法应该被调用。“reachable”意味着对象是可达的。换言之,可将freachable队列当作是像静态字段那样的一个根。所以,若是一个对象在freachable队列中,它就是可达的,
不是垃圾。
简单地说,当一个对象不可达,垃圾回收器就把它视为垃圾。可是,当垃圾回收器将对象的引用从终结列表移至freachable队列时,对象再也不被认为是垃圾,其内存不能被回收。标记freachable对象时,这些对象的引用类型的字段也会被递归地标记;全部这些对象都会在垃圾回收过程当中存活下来。到这个时候,垃圾回收器才结束对垃圾的标识。因为一些本来被认为是垃圾的对象被从新认为不是垃圾,因此从某种意义上说,这些对象“复活”了。而后,垃圾回收器开始压缩(compact, 使得紧凑)可回收的内存,特殊的CLR线程清空freachable队列,并执行每一个对象的Finalize方法。
垃圾回收器下一次调用时,会发现已经终结的对象称为真正的垃圾,由于应用程序的根再也不指向它,freachable队列也再也不指向它。因此,这些对象的内存会直接回收。整个过程当中,注意可终结的对象须要执行两次垃圾回收才能释放它们占用的内存。实际应用中,因为对象可能被提高至另外一代,因此可能要求不止进行两次垃圾回收(代的问题之后详述)。以下图展现了第二次垃圾回收后托管堆的状况:
Dispose 模式:强制对象清理资源
Finalize方法很是有用,由于它确保了当托管对象的内存被释放时,本地资源不会泄露。可是,Finalize方法问题在于,它的调用时间是不能保证的。另外,因为它不是公共方法,因此类的用户不能显示调用它。
使用包装了本地资源(好比文件、数据库链接和位图等)的托管类型时,肯定性地dispose或关闭对象的能力一般都是颇有用的。例如,你可能想打开一个数据库链接,查询一些记录,而后关闭该数据库链接 -- 在发生下一次垃圾回收以前,你不但愿数据库链接一直处于打开状态,尤为是下一次垃圾回收可能在你获取了数据库记录的几小时或者几天以后才会发生。
类型为了提供肯定性dispose或者关闭对象的能力,要实现所谓的Dispose模式。类型为了提供显式进行资源清理的能力,必须遵照Dispose模式定义的规范。除此以外,若是一个类型实现了Dispose模式,使用该类型的开发人员就能够准确地知道在对象不须要时,如何显式地dispose它。
注意:
全部定义了Finalize方法的类型都应同时实现本节描述的Dispose模式,使类型的用户对资源的生存期有更多的控制,可是,类型也可实现Dispose模式,但不定义Finalize方法。例如,System.IO.BinaryWriter就是这样的类型。
System.IDispose的定义以下:
public

interface IDispose
{
void Dispose();
}
任何类型只要实现了该接口,就至关于声称本身遵循Dispose模式。简单地说,这意味着类型提供了一个公共无参Dispose方法,可显式调用它来释放对象包装的资源。注意,对象自己的内存不会从托管堆的内存中释放,仍然要由垃圾回收器负责释放对象的内存,并且具体时间不定。提供了Dispose模式的一些类型为了方便起见,还提供了一个Close方法,而只是调用Dispose方法,但这对于Dispose模式来讲并不是必须。如System.IO.FileStream类提供了Dispose模式,也提供了Close方法。然而,System.Threading.Timer类就没有提供Close方法,虽然它也遵循Dispose模式。
实现了Dispose模式的类型
C# 的using语句
说明:
dispose,在英语语境中,它的意思是“摆脱”或者“除去”(get rid of)一个东西,尤为是在这个东西很难除去的状况下。
在.NET Framework 文档中,它的官方翻译是“释放”,意思是显式释放或者清理对象包装的资源。
之因此认为“释放”不恰当,除了和release一词冲突以外,还由于dispose强调了“清理资源”,并且在完成(它包装)资源的清理以后,对象自己的内存并不会释放。
因此,“dispose一个对象”或者"close一个对象" 真正的意思是: 清理对象中包装的资源(好比它的字段所引用的对象),而后等待垃圾回收器自动回收该对象自己所占用的内存。