作为web方面的应用层开发人员,不可能和系统程序员一般对操作系统底层运行原理和机制一清二楚,毕竟不同类型的开发者工作性质不同,也有不同的知识结构,例如要求c程序员去实现论坛,或要求web开发人员精通cpu调度算法,页面调度算法等都不现实。但是对于内存管理、线程安全这些必须的“基础”知识,是非常有必要去学习清楚的,这是作为一个合格的程序员的基础。
(ps:文章主要内容以及测试Demo大多来源于《CLR.via.C# 第三版》中的自动内存管理GC)。
一、理解垃圾回收平台的基本工作原理
为什么需要垃圾回收器?
我们都知道每个程序都要使用这样或那样的资源,比如文件、内存缓冲区、屏幕空间、网络连接、数据库资源等。(C#/JAVA等高级语言都是封装了本地资源供开发者使用,想要了解具体有哪些API是如何封装这些本地资源的,可以看我另一篇博客)。事实上,在面向对象的环境,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存。以下是访问一个资源所需的具体步骤。
1、调用 IL指令 newobj,为代表资源的类型分配内存。在C#中使用 new操作符,编泽器就会自动生成该指令。
2、初始化内存, 设置资源的初始状态, 使资源可用。类型的实例构造器负责设置该初始状态。
3、访问类型的成员(可根据需要反复)来使用资源
4、摧毀资源的状态以进行清理
5、释放内存。这是垃圾回收器的主要任务。
这种看似简単的模式却是导致编程错误的“元凶”之一。想想看,有多少次程序员忘记释放不再需要的内存? 又有多少次程序员试图使用已被释放的内存? 进行非托管编程时, 应用程序的这两种bug比其他大多数 bug都要严重, 因为一般无法预测它们的后果或者发生的时间。如果是其他 bug, 一旦发现程序行为异常,修正问题即可。但是,这两种 bug会造成资源泄源(浪费内存)和对象损坏(影响稳定性), 这使应用程序的表现变得不可预测。
特别是对于应用层开发者而言,正确进行资源管理通常很难面而且很枯燥,它会极大分散开发人员的注意力,使之无法专注于真正要解决的问题。如果有-一种机制能为开发人员简化这种令人厌恶的内存管理任务,那将是一大幸事。幸好, 确实存在这样的机制, 这就是垃圾回收(garbage collection)。
垃圾回收使开发发人员得到了解放, 现在不必跟踪内存的使用, 也不必知道在什么时候释放内存。但是,垃圾回收器对内存中的类型所代表的资源是一无所知的。这意味着垃圾回收器不知道怎样执行前面的第4步: 摧毀资源的状态以进行清理。为了使资源得到正确清理,开发人员必须编写知道如何正确清理资源的代码。这些代码放在finalize, Dispose和 Close方法中。
另外要注意,值类型(含所有枚举类型)、集合类型、 String、 Attribute、 Delegate和 Exception 所代表的资源无需执行特殊的清理操作。例如, 只需销毀对象的内存中维护的字符数组, 一个 String资源就会被完全清理。
另一方面,如果一个类型代表者(或包装着)一个非托管资源或者本地资源(比如文件、数据库連接、套接字、 mutex、位图、图标等),那么在对象的内存准备回收时,必须执行一些资源清理代码。我们将学习如何正确地定义需要显式清理的类型,还将展示如何正确使用提供了显式清理的类型。下面,让我们先来探讨一下内存分配和资源初始化问题。
从托管堆分配资源
CLR要求所有的资源都从托管堆(managed heap)分配。这个堆和 C运行时堆非常相似,只是你永远不从托管堆中删除对象,因为应用程序不需要的对象会自动刷除。自然引出了一个问题:“托管堆如何知道应用程序不再用一个对象? ”稍后就会回答这个问题 。目前使用的垃圾回收算法有好几种。 每种算法都针对特定的环境进行了优化, 能提供这种环境下最佳的性能。这里讨论的是 Microsoft .NET Framework的 CLR所采用的垃圾回收算法。
先从最基本的概念讲起。进程初始化时, CLR要保留-一块连续的地址空同, 这个地址空间最初并没有对应的物理存储空间。这个地址空间就是托管堆。托管堆还维护着一个指针,我把它称为 NextObjPtr。它指向下一个对象在堆中的分配位置。刚开始的时候, NextObjPtr设为保留地址空问的基地址。当开始创建一个对象的时候,IL指令 newobj用于创建一个对象。许多语言(包括C#,C++fCLI以及 Microsoft Visua1 Basic) 都提供了一个 new操作符,它导致编译器在方法的lL 代码中生成一个 newobj指令。newobj 指令将导致 CLR执行以下步骤.
1. 计算类型(及其所有基类型)的字段需要的字节数。
2. 加上对象的开销所需的字节数。每个对象都有两个销字段: 一个是类型对象指针,和一个同步块索引。
3. CLR检查保留区域是否能够提供分配对象所需的字节数,如有必要就提交存储(也即是将存储空间“交”给预定 者)。如果托管堆有足够的可用空间, 对象会被放入。注意对象是在 NextObjPtr指针指向的地址放入的,并且为 它分配的字节会被清零。接着,调用类型的实例构造器(为 this参数传NextObjptr), IL指令 newobj(或者 C# new 操作符)将返同对象的地址。就在地址返回之前, NextObjptr指针的値会加上对象占据的字节数, 这样会得到一 个新值, 它指向下一个对象放入托管堆时的地址。
图1-l展示了3个对象(A,B和 C)的一个托管堆。如果要分配新对象,它将放在 Ne1【tObjptr 指针指向的位置(紧接在对象 c后)。
NenObj Ptr
图1-1 新初始化的托管堆,其中构造了3个对象
在托管堆中, 连续分配的对象可以确保它们在内存中是连续的。托管堆做了一个相当大胆的假设一地址空间和存储是无限的。这个假设显然是荒谬的。所以, 托管堆必须通过某种机制来允许它做这样的假设。这个机制就是垃圾回收器。下面讲述它的工作原理 。
应用程序调用 new操作符创建对象时, 可能没有足够的地址空间来分配该对象。 托管堆将对象需要的字节数加到NextObjptr指针中的地址上来检测这种情況 。如果结果值超过地址空间的末尾, 表明托管堆已满, 必须执行一次垃圾回收。当然这也是高度概括般的描述。事实上,垃级回收是在第 0代满的时候发生的。关于代的概念,这里先不做描述,只需知道第0代就是最近分配的对象,从未被垃圾回收算法检査过。(垃圾回收器使用了代(generation)的机制, 该机制唯一的目的就是提升性能。将对象划分为代, 使垃圾回收器能专注于回收特定的代, 而不是每次都要回收托管难中的所有对象。)
三、垃圾回收算法
垃圾回收器检査托管堆中是否有应用程序不再使用的任何对象。如果有,它们使用的内存就可以回收(如果一次拉圾回收之后, 堆中仍然没有可用的内存, new操作符将会抛出一个0utOfMemoryException)。垃圾回收器如何知道应用程序正在使用一个对象?
每个应用程序都包含一组根(root)。 每个根都是一个存储位置, 其中包含指向引用类型对象的一个指针。该指针要么引用托管堆中的一个对象,要么为 null。例如,类型中定义的任何静态字段被认为是一个根。除此之外,任何方法參数或局部变量也被认为是一个根。只有引用类型的变量才被认为是根;值类型的变量永远不被认为是根。让我们看一个具体的例子,首先是类定义:
internal sealed class SomeType
{
private TextWriter m_textWriter;
public SomeType(TextWriter tw)
{
m_textWriter=tw;
}
public void WriteBytes(Byte[] bytes)
{
for(int x=0;x
{
m_textWriter.Write(bytes[x]);
}
}
}
WriteBytes 方法第一次调用时, JIT编译器将方法的IL 代码转换成本地 CPU指令。JIT编译器生成本地代码时, 还会创建一个内部使用的表。从逻辑上来讲, 该表中的每个记录项都代表在方法的本地CPU指令中的一个字节偏移范围。针对每个范围,这个记录项都记录了包含着根的一组内存地址和 CPU寄存器。垃圾回收器可以沿着线程的调用栈上行,检査每个方法的内部表来确定所有调用方法的根。最后, 垃圾回收器遍历所有类型对象来获取静态字段中存储的根集合。
垃圾回收器开始执行时,它假设堆中所有对象都是垃圾。换句话说,它假设线程栈中没有引用了堆中对象的变量,没有CPU寄存器引用堆中的对象,也没有静态字段引用堆中的对象。垃圾回收器的第一个阶段是所调的标记(marking)阶段。在这个阶段,垃圾回收器沿着线程栈上行以检査所有根。如果发现一个根引用了一个对象,就在对象的 “同步块索引字段”上开启一位----对象就是这样“标记”的。例如,垃圾回收器可能发现一个局部变量指向堆中的一个对象。图l-2展示了一个堆,其中包含几个已分配的对象。应用程序的根直接引用对象A, C, D和F。所有这些对象都已标记。标记对象D时,垃圾回收器发现这个对象含有一个引用对象 H的字段,造成对象 H也被标记。垃圾回收器就是这样, 以递归的方式通历所有可达的对象。
图1-2 回收之前的托管堆
标记好根和它的字段引用的对象之后,垃圾回收器检査下一个根,并继续标记对象。如果垃圾回收器试图标记一个先前标记过的对象, 就会停止沿着这个路径走下去。这个行为有两个目的。首先,垃圾回收器不会多次遍历一组对象,所以性能得到显著增强。其次,如果存在对象的循环链表,可以避免陷入无限循环。
检查好所有根之后, 堆中将包含一组已标记和未标记的对象。己标记的对象是通过应用程序的代码可达的对象,而未标记的对象是不可达的。不可达的对象被认为是垃圾,它们占用的内存可以回收。现在,垃圾回收器开始第二个阶段, 即压缩(compact)阶段。在这个阶段中, 垃圾回收器线性地遍历堆,以寻找未标记(垃圾)对象的连续内存块。
如果发现的内存块比较小, 垃圾回收器会忽略它们。但是, 如果发现大的、可用的连续内存块,垃圾回收器会把非垃圾的对象移动到这里以压缩堆。
很自然, 移动内存中的对象之后, 包含“指向这些对象的指针”的変量和 CPU寄存器现在都会变得无效。所以, 垃圾回收器必须重新访问应用程序的所有根, 并修改它们来指向对象的新内存位置。另外, 如果对象中的字段指向的是另一个己移动了位置的对象,垃圾回收器也要负责改正这些字段。堆内存压缩之后,托管堆的NextObjptr指针将指向紧接在最后一个非垃圾对象之后的位置。图21-4展示了一次垃圾回收后的托管堆 。
图1-3 垃圾回收后的托管堆
如你所见,垃圾回收会造成显著的性能损失,这是使用托管堆的主要缺点。但要注意的是, 垃圾回收只在第 0代满的时候才发生。
作为程序员,应该从前面的讨论中得出两点重要的认识。第一点,不必自己实现代码来管理应用程序所用的对象的生存期。第二点,文章开始描述的两种bug将不复存在。首先,不可能再发生对象泄漏的情況,因为任何对象只要没有应用程序的根引用它,都会在某个时刻被垃圾回收器回收,所以应用程序将不可能再发生内存泄漏的情况。另外,应用程序也不可能再访问已经被释放的对象。这是因为假如对象可达,就不会被释放;假如它不可达,应用程序就没办法访问它。另外,由子垃圾回收导致了内存的压缩(compact), 所以托管对象不可能造成进程虚拟地址空间的碎片化。如果是非托管堆,地址空间的碎片化现象可能非常严重。一个例外是在使用大对象的时候(以后将进行讨论), 大对象堆仍是有可能碎片化的。
二、垃圾回收与调试
通过上面垃圾回收算法原理,我们知道只要对象变得不可达,就会成为垃圾回收器的目标。下面先看一个例子:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace TestGC
{
class Program
{
static void Main(string[] args)
{
Timer t = new Timer(TimerCallback,null,0,2000);
Console.ReadLine();
}
private static void TimerCallback(Object o)
{
Console.WriteLine("In TimerCallback:"+DateTime.Now);
GC.Collect();
}
}
}
在命令行中编译并运行,结果如下:
我们会发现 TimerCallback方法只被调用了一次!(注意:如果是在vs2010中运行这段代码,会发现TimerCallback方法得到不断的运行,这是因为JIT编译器做了某些优化以便帮助调试,只要方法被调用,JIT会把变量的生存期延长到方法结束)
观察上述代码,你可能会认为 TimerCallback方法每隔2000毫秒就调用一次。毕竟,上述代码创建了-一个 Timer对象, 而且有一个变量 t引用该对象。只要计时器对象存在, 计时器就应该一直触发。但请注意,在 TimerCallback方法中,通过调用 GC.ColIect()强制执行了一次垃圾回收。
回收开始时,垃圾回收器首先假定堆中的所有对象都是不可达的(垃圾);这当然也包括Timer对象。然后,垃圾回收器检查应用程序的根,发现在初始化之后, Main方法再也没有用过变量 t。既然应用程序没有任何变量引用 Timer对象,垃圾回收就回收分配给它的内存,这使计时器停止触发, 并解释了为什么 TimerCallback方法只被调用了一次。
如果想程序正常运行,可以在Console.ReadLine();后面加入一条语句t.Dispose();因为t引用的对象必须存活,才能在它上面调用 Dispose实例方法(t中的值要作为 this实参传给Dispose())
三、使用终结操作类释放本地资源
通过以上知识,基本可以了解了垃圾回收和托管堆的情况,包过垃圾回收器如何回收对象的内存。尽管大多数类型只需内存就可以正常工作,但是,也有一些类型除了要使用内存,还使用本地资源。例如, System.IO.FileStream类型需要打开一个文件(本地资源)并保存文件的句柄。然后, 该类型的 Read和 Write方法用该句柄来操作文件。类似地, System.Thrading.Mutex类型要打开一个 Windows互斥体内核对象(本地资源)并保存其句柄, 并在调用 Mutex的方法时使用该句柄。
终结(fiinalization)是 CLR 提供的一种机制, 允许对象在垃圾回收器回收其内存之前执行一些得体的清理工作。任何包装了本地资源(例如文件、网络连接、套接字、互斥体或其他类型)的类型都必须支持终结操作。简単地说,类型实现一个命名为Finalize()的方法。当垃圾回收器判断一个对象是垃圾时,会调用对象的 Finalize方法(如果有的活)。可以这样理解: 实现了 Finalize方法的任何类型实际上是在说, 它的所有对象都希望在“被处决之前吃上最后一餐” 。
Microsoft 的 C#团队认为, Finalize 方法是在编程语言中需要特殊语法的一种方法(类似于C#要求用特殊的语法定义构造器)。因此,在 C#中,必须在类名前加一个~符号来定义Finalize方法,如下例所示:
internal sealed class SomeType
{
//这是一个Finalize方法
~SomeType()
{
Console.WriteLine("这里的代码会进入Finalize方法");
}
static void Main(string[] args)
{
Console.ReadLine();
}
}
编译上述代码,用 ILDasm.exe查看得到的程序集,会发现 C#编译器实际是在模块的元数据中生成一个名为 Finalize的 protected override方法。査看 Finalize的 IL代码,会发现方法主体的代码被放到一个 try 块中, fina11y块则放入了一个对 base.Finalize的调用。如:
实现 Finalize方法时,一般都会调用 Win32 CloseHandle函数,并向该函数传递本地资源的句柄。例如, FileStream类型定义了一个文件句柄字段,它标识了本地资源。FileStream 类型还定义了一个Finalize方法, 它在内部调用 CloseHandle函数,并向它传递文件句柄字段。这就确保了在托管的 FileStream对象被确定为垃圾后, 本地文件句柄会得以关闭。如果包装了本地资源的类型没有定义 Finalize 方法,本地资源就得不到关闭, 导致资源泄漏, 直至进程终止 。进程终止时, 这些本地资源才会被操作系统回收。
四、什么时候会导致Finalize方法。
Finalize方法在垃圾回收结束时调用,主要有一下5种事件会导致开始垃圾回收
1、 第0代满
第 0代满时, 垃圾回收会自动开始。该事件是目前导致 Finalize方法被调用的最常见的一种方式, 因为随着应用程序代码运行并分配新对象, 这个事件会自然而然地发生。
2、 代码显示调用System.GC的静态方法Collect()
虽然 Microsoft 强烈建议不要这样做,但是某些时候还是有必要。
3、 Windows报告内存不足
CLR内部使用 Win32 CreateMemoryResoureeNotification和QueryMemoryResourceNotification函数来监视系统的总体内存。如果 Windows报告内存不足,CLR将强制执行垃圾回收,尝试释放已经死亡的对象,从而减小进程工作集的大小。
4、 CLR卸载AppDomain
一个 AppDomain被卸载时,CLR认为该 AppDomain中不再存在任何根,因此会对所有代的对象执行垃圾回收。
5、 CLR关闭
一个进程正常终止时(相对于从外部关闭,比如通过任务管理器关闭),CLR就会关闭。在关闭过程中,CLR会认为该进程中不存在任何根,因此会调用托管堆中的所有对象的Finalize方法。注意, CLR此时不会尝试圧缩或释放内存,因为整个进程都要终止, 将由 Windows负责回收进程的所有内存。
注意无法控制Finalize方法在什么时候运行。Finalize方法在垃圾回收发生时运行,而垃圾回收可能在应用程序请求更多内存时发生。另外, CLR不保证各个Finalize方法的调用顺序。因此,在写一个Finalize方法时,应避免访问定义Finalize方法的其他类型的对象: 那些对象可能已经终结了。然而, 完全可以放心地访问值类型的实例, 或者访问没有定义Finalize方法的引用类型的对象。调用静态方法也要当心, 因为这些方法可能在内部访问已终结的对象, 导致静态方法的行为变得无法预测 。
五、终结操作解密
终结操作表面上似乎简单:开始创建一个对象时,分配内存,当它被回收时,它的Finalize方法会得到调用。但一旦深究下去,会发现终结操作远非这么简单。详情可见《CLR.via.C# 第三版》----自动内存管理章节。这里仅仅是稍微概括一下:
应用程序创建一个新对象时, new 操作符会从堆中分配内存。如果对象的类型定义了Finalize方法,那么在该类型的实例构造器被调用之前,会将指向该对象的一个指针放到一个终结列表(Finalization List)。终结列表是垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象,在回收该对象的内存之前, 应调用它的 Finalize方法。
图1-5展示了包含几个对象的一个堆。有的对象从应用程序的根可达,有的则不然。对象C, E, F, I和 J被创建时,系统检测到这些对象的类型定义了 Finalize方法,所以将指向这些对象的指针添加到终结列表中。
图1-5
垃圾回收开始时,对象B, E, G, H, I和J被判定为垃圾。垃圾回收器扫描终结列表以查找指向这些对象的指针。找到一个指针后,该指针会从终结列表中移除,并追加到freachable 队列中。freachable 队列是垃圾回收器的另一个内部数据结构。freachable队列中的每个指针都代表其FinaIize方法已准备好调用的一个对象。图l-6展示了回收完毕后的托管堆的情况。从图1-6可以看出,对象B, G和 H占用的内存已被回收, 因为它们没有 FinaIize方法。但是,对象 E,I和 J占用的内存暂时不能回收, 因为它们的 FinaIize方法还没有调用。
图1-6托管堆中, 一些指针从终结列表移至freachable队列
简单地说,当一个对象不可达时,垃圾回收器就把它视为垃圾。但是,当垃圾回收器将对象的引用从终结列表移至freachable队列时,对象不被认为是垃圾,其内存不能被回收。标记freachable对象时, 这些对象的引用类型的字段所引用的对象也会被递归地标记;所有这些对象都会在垃圾回收过程中存活下来。到这个时候, 垃圾回收器才结束对垃圾的标识。由于一些原本被认为是垃圾的对象被重新认为不是垃圾,所以从某种意义上讲,这些对象“复活”了。然后,垃圾回收器开始压缩(compact)可回收的内存,特殊的 CLR线程清空freachable队列, 并执行每个对象的 Finalize 方法。
垃圾回收器下一次被调用时,会发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachable队列也不再指向它。所以,这些对象的内存会直接回收。这个过程中, 注意可终结的对象需要执行两次垃圾回收才能释放它们占用的内存。在实际应用中, 由子对象可能被提升至另一代, 所以可能要求不止进行两次垃圾回收。图1-7展示了第二次垃圾回收后托管堆的情况。
图1-7第二次垃圾回收后托管堆的情况
七、Dispose模式:强制对象清理资源
Finalize方法非常有用,因为它确保了当托管对象的内存被释放时,本地资源不会泄漏。但是, Finalize方法的问题在于,它的调用时间是不能保证的。另外,由于它不是公共方法, 所以类的用户不能显式调用它。
使用包装了本地资源(比如文件、 数据库连接和位图等)的托管类型时, 确定性地Dispose或Close关闭对象(dispose一个对象或者close 一个对象真正的意思是:清理对象中包装的资源 (比如说:它的字段所引用的对象). 然后等待垃圾回收器自动回收该对象本身占用的内存)的能力通常都是很有用的。例如, 你可能想打开一个数据库连接,查询一些记录,然后关闭该数据库连接。因为在发生下一次垃圾回收之前,你不希望数据库连接一直处于打开状态,尤其是下一次垃圾回收可能在你获取了数据库记录的几小时或几天之后才会发生。类型为了提供确定性 dispose或关闭对象的能力,要实现所调的 Dispose模式。类型为了提供显式进行资源清理的能力,必须遵守 Dispose模式定义的规范。除此之外,如果一个类型实现Dispose模式,使用该类型的开发人员就可以准确地知道在对象不需要时,如何显式地 dispose。
Dispose模式的实现比较繁琐。这里不做过多的讨论,仅仅是概括性的描述。要实现Dispose模式可以实现System.IDisposable接口, 该接口在 FCL中的定义如下:
Public interface IDisposable{
void Dispose() ;
}
任何类型只要实现该接口,就相当于声称自己遵循 Dispose模式。简単地说,这意味着类型提供一个公共无参 Dispose方法,可显式调用它来释放对象包装的资源。注意,对象本身的内存不会从托管堆的内存中释放;仍然要由垃圾回收器负责释放对象的内存, 而且具体发生时间不定。
六、使用实现了Dispose模式的类型
想深入了解如何实現 Dispose模式还可以去找资料来研究研究,这里重点是来看一下开发人员怎样使用提供了Dispose模式的类型。这里讨论的是System.I〇.FileStream类。可利用 FileStream类打开一个文件,从文件中读取字节,向文件中写入字节,并关闭文件。一个 FileStream对象在构造时,它会调用 Win32 CreateFile函数,该函数返回的句柄保存在SafeFileHandle对象中, 然后通过 FileStream对象的一个私有字段来维护运载该对象的引用。 FileStream类还提供子几个额外的属性(例如 Length, Position, CanRead等)和方法(例如Read, Write, Flush等)。
假设要写代码来创建一个临时文件, 并向其中写入一些字节,然后再删除该文件。
class Program
{
static void Main(string[] args)
{
Byte[] bytesToWrite =new Byte[ ]{1,2,3,4,5};
FileStream fs = new FileStream("temp.dat",FileMode.Create);
fs.Write(bytesToWrite, 0, bytesToWrite.Length);
File.Delete("temp.dat");
}
}
运行结果:
幸好,FileStream类实现了Dispose 模式,修改之后的源代码。所以可以修改代码来显式地关闭文件。
class Program
{
static void Main(string[] args)
{
Byte[] bytesToWrite =new Byte[ ]{1,2,3,4,5};
FileStream fs = new FileStream("temp.dat",FileMode.Create);
fs.Write(bytesToWrite, 0, bytesToWrite.Length);
fs.Dispose();
File.Delete("temp.dat");
}
}
上述代码唯一的区别是添加了对 FileStream的 Dispose方法的调用。Dispose方法调用接受一个bool参数的 Dispose方法,后者在 SafeFileHandle对象上调用 Dispose方法,该方法调用 Win32CloseHandle函数,造成Windows关闭文件。然后,调用 File的 Delete方法时, windows发现该文件已经关闭, 所以能成功地删除它 。
需要注意的是,调用 Dispose或 Close只是为了能在一个确定的时间强迫对象执行清理:这两个方法并不能控制托管堆中的对象所占用的内存的生存期。这意味着即使一个对象已完成了清理,仍可在它上面调用方法。以下代码在文件关闭后调用 Write方法, 试图向文件写入更多的字节。显然,这些字节无法再写入文件。
class Program
{
static void Main(string[] args)
{
Byte[] bytesToWrite =new Byte[ ]{1,2,3,4,5};
FileStream fs = new FileStream("temp.dat",FileMode.Create);
fs.Write(bytesToWrite, 0, bytesToWrite.Length);
fs.Dispose();
fs.Write(bytesToWrite, 0, bytesToWrite.Length);
File.Delete("temp.dat");
}
}
运行结果:
这里不会出现内存损坏的情况,因为FileStream对象的内存依然“健在”。只是在执行了清理之后, 对象不能再成功地执行它的方法而已
七、C#的Using语句
前面的代码示例展示了怎样显式调用一个类型的 Dispose或 Close方法。如果决定显式地调用这两个方法之一,强烈建议把它们放在一个异常处理 finally块中。这样可以保证清理代码得到执行。 因此, 前代码示例可以修改成下面这种更好的形式:
class Program
{
static void Main(string[] args)
{
Byte[] bytesToWrite =new Byte[ ]{1,2,3,4,5};
FileStream fs = new FileStream("temp.dat",FileMode.Create);
try
{
fs.Write(bytesToWrite, 0, bytesToWrite.Length);
}
finally
{
if (fs != null) fs.Dispose();
}
File.Delete("temp.dat");
}
}
然而,C#语言提供了一个 using语句, 它提供了一种简化的语句来获得和上述代码相同的结果。下面演示如何使用 c#的 using 语句重写上述代码:
class Program
{
static void Main(string[] args)
{
Byte[] bytesToWrite =new Byte[ ]{1,2,3,4,5};
using (FileStream fs = new FileStream("temp.dat", FileMode.Create))
{
fs.Write(bytesToWrite, 0, bytesToWrite.Length);
}
File.Delete("temp.dat");
}
}
在 using语句中,我们初始化一个对象,并将它的引用保存到一个变量中。然后在 using 语句的大括号内访问该变量。 编译这段代码时, 编译器自动生成一个 try 块和一个finally块。在finally块中,编译器会生成代码将变量转型成一个 IDisposable并调用 Dispose方法。 显然, using语句只能用于那些实现了 IDisposable接口的类型。
用ILSpy反编译看IL语言,如下:
using语句也能用于实现了 IDisposable接口的值类型。这样一来,我们就可以创建一个非常高效和有用的机制来封装 “开始和结束一个操作” 所需的代码。例如,假设要用一个Mutex对象来锁定一个代码。Mutex类确实实现了 IDisposable接口,但是在它上面调用Dispose只会释放本地资源:不会对锁本身进行任何处理。为了简化锁定和解锁一一个 Mutex的操作,可以定义一个值类型来封装 Mutex对象的锁定和解锁操作。下面的 MutexLock结构就是一个例子, 随后的 Main方法演示了如何高效地使用 MutexLock结构
internal struct MutexLock : IDisposable
{
private readonly Mutex m_mutex;
public MutexLock(Mutex m)
{
m_mutex = m;
m_mutex.WaitOne();
}
public void Dispose()
{
m_mutex.ReleaseMutex();
}
}
class Program
{
static void Main(string[] args)
{
Mutex m = new Mutex();
using (new MutexLock(m))
{
Console.WriteLine("do something");
}
}
}
八、手动监视和控制对象的生存期
CLR为每一个AppDomain都提供了一个GC句柄表(GC Handle table)。该表允许程序监控对象的生命期,或手动控制对象的生存期。详情可见《CLR.via.C# 第三版》----自动内存管理章节。
九、代
代(generation)是CLR垃圾回收器采取的一种机制,它唯一的目的就是提升应用程序的性能。一个基于代的垃圾回收器(generation garbage collector)做出了以下几点假设:
1、 对象越新,生存期越短;
2、 对象越老,生存期越长;
3、 回收堆的一部分,速度快于回收整个堆
经过研究证明,对于现今的大多数应用程序,这些假设都是成立的。而且些假设影响了垃圾回收器实现方式。托管堆在初始化时不包含任何对象。添加到堆的对象成为第0代对象。简单的说,第0代对象就是那一批新构造的对象,垃圾回收器还没有检查过它们。下图展示了一个新启动应用程序,
十、用于本地资源的其他垃圾回收功能
有一些本地资源会消耗大量内存,但用于封装该资源的托管对象只占用了非常少的内存。例如位图,一个位图可能占用几兆字节的本地内存,但托管对象却极小。从CLR的角度看,一个进程可以在执行一次垃圾回收之前分配数百个位图(因为它们用的托管内存实在是太少了)。但是,如果进程操作许多位图,进程的内存消耗将一一个恐怖的速度增长。针对这种问题,GC类提供了以下两个静态方法:
//通知运行库在安排垃圾回收时应考虑分配大量的非托管内存
//参数:bytesAllocated,已分配的非托管内存的增量
public static void AddMemoryPressure(long bytesAllocated);
// 通知运行库已释放非托管内存,在安排垃圾回收时不需要再考虑它
//参数:bytesAllocated,已释放的非托管内存量
public static void RemoveMemoryPressure(long bytesAllocated);
如果一个类要包装可能很大的本地资源,就应该使用这些方法提示垃圾回收器实际要需要消耗多少内存。这样的话,垃圾回收器内部会监视内存压力,压力变大时,就会强制执行垃圾回收。
十一、编程控制垃圾回收器
System.GC 类型允许应用程序在某种程度上直接控制垃圾回收器。例如:可以调用一下静态方法之一来强迫执行一次垃圾回收:
void GC.Collect();//强制对所有带执行一次完全回收
void GC.Collect(int generation); //指定回收generation及以下的代
void GC.Collect(int generation, GCCollectionMode mode);// GCCollectionMode枚举类型有三种,最常用的就是Optimized,表示只有在能够释放大量内存或者能减少碎片化的前提下,才执行回收。
在大多数情况下,都应避免调用任何一个Collect方法,最好是让垃圾回收器自动判断执行。当然也有一些时候,可能希望建议在特定的试卷发生一次垃圾回收,例如对一些CUI(console user interface,控制台用户界面)或GUI(graphical user interface,图形用户界面)应用程序,这些应用程序代码拥有进程和进程中的CLR。这种情况下,建议把GCCollectionMode设为Optimized。
假如刚才发生了某个非重复性的事件,并导致大量旧对象死亡,就可以考虑手动调用一次Collect()。因为对于非重复性的事件,垃圾回收器基于历史而对未来的预测可能会不准。
例如,在应用程序初始化完成以后,或者在用户保持了一个数据文件后,应用程序就适合对所有带都强制执行一次完全的垃圾回收。但不要为了改善应用程序的响应时间而显示调用Collect(),我们的目标应该是出于对减少进程工作集的目的而调用它。
另外,GC类还提供了两个静态方法来帮助你判断对象当前在哪一代中:
int GetGeneration(object obj)//obj为对象引用,返回0、1、2三者其一
int GetGeneration(WeakReference wo)
下面通过一段代码来演示关于GC类的方法在代工作原理中发挥的作用:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Maximum generations :"+GC.MaxGeneration);// 托管堆支持最大代数
//在堆中创建一个新的GenObj
Object o = new GenObj();
//因为这个对象是新创建的,所以在第0代中
Console.WriteLine("Gen"+GC.GetGeneration(o));
//执行垃圾回收,提升对象的代¨²
GC.Collect();
Console.WriteLine("Gen"+GC.GetGeneration(o));
GC.Collect();
Console.WriteLine("Gen" + GC.GetGeneration(o));
GC.Collect();
Console.WriteLine("Gen" + GC.GetGeneration(o));//最多是第二代
o = null;//销毁对象的强引用
Console.WriteLine("Collect Gen 0");
GC.Collect(0);//回收第0代
GC.WaitForPendingFinalizers();//Finalize未调用
Console.WriteLine("Collect Gen 0,and 1");
GC.Collect(1);
GC.WaitForPendingFinalizers();//Finalize未调用
Console.WriteLine("Collect Gen 0,and 1,and 2");
GC.Collect(2);//等同于Collect()
GC.WaitForPendingFinalizers();//Finalize被调用
}
}
internal sealed class GenObj
{
~GenObj()
{
Console.WriteLine("In Finalize method");
}
}
编译运行结果如下所示: