C# .net中的垃圾收集
Heap内存经过回收、压缩之后,可以继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就可以 主要处理步骤:将线程挂起=确定roots=创建reachable objectsgraph=对象回收=heap压缩=指针修复 可以这样理解roots:heap中对象的引用关系错综复杂(交叉引用、循环引用),形成复杂的graph,roots是CLR在heap之外可以找到的各种入口点。
GC搜索roots的地方包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalizationqueue)等。主要可以归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(stack+CPU register) Reachable objects:指根据对象引用关系,从roots出发可以到达的对象。例如当前执行函数的局部变量对象A是一个rootobject,他的成员变量引用了对象B,则B是一个reachable object。从roots出发可以创建reachable objectsgraph,剩余对象即为unreachable,可以被回收 指针修复是因为compact过程移动了heap对象,对象地址发生变化,需要修复所有引用指针,包括stack、CPUregister中的指针以及heap中其他对象的引用指针 Debug和release执行模式之间稍有区别,release模式下后续代码没有引用的对象是unreachable的,而debug模式下需要等到当前函数执行完毕,这些对象才会成为unreachable,目的是为了调试时跟踪局部对象的内容 传给了COM+的托管对象也会成为root,并且具有一个引用计数器以兼容COM+的内存管理机制,引用计数器为0时这些对象才可能成为被回收对象 Pinnedobjects指分配之后不能移动位置的对象,例如传递给非托管代码的对象(或者使用了fixed关键字),GC在指针修复时无法修改非托管代码中的引用指针,因此将这些对象移动将发生异常。pinnedobjects会导致heap出现碎片,但大部分情况来说传给非托管代码的对象应当在GC时能够被回收掉。?
二、 Generational 分代算法
程序可能使用几百M、几G的内存,对这样的内存区域进行GC操作成本很高,分代算法具备一定统计学基础,对GC的性能改善效果比较明显 将对象按照生命周期分成新的、老的,根据统计分布规律所反映的结果,可以对新、老区域采用不同的回收策略和算法,加强对新区域的回收处理力度,争取在较短时间间隔、较小的内存区域内,以较低成本将执行路径上大量新近抛弃不再使用的局部对象及时回收掉 分代算法的假设前提条件:
1、大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长
2、对部分内存进行回收比基于全部内存的回收操作要快
3、新创建的对象之间关联程度通常较强。heap分配的对象是连续的,关联度较强有利于提高CPU cache的命中率 .NET将heap分成3个代龄区域: Gen 0、Gen 1、Gen 2 Heap分为3个代龄区域,相应的GC有3种方式: # Gen 0 collections, # Gen 1 collections, #Gen 2 collections。如果Gen 0 heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1。如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2。
2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收 Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为fullGC,通常成本很高。粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时fullGC可能需要花费几秒时间。大致上来讲.NET应用运行期间2代、1代和0代GC的频率应当大致为1:10:100。
三、Finalization Queue和Freachable Queue
这两个队列和.net对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象,而是存储一组指向对象的指针。当程序中使用了new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。
在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为是对象的复生(Resurrection),本来死去的对象就这样被救活了。为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以不能让它死去。Freachable Queue平时不做什么事,但是一旦里面被添加了指针之后,它就会去触发所指对象的Finalize方法执行,之后将这个指针从队列中剔除,这是对象就可以安静的死去了。
.net framework的System.GC类提供了控制Finalize的两个方法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对象的指针重新添加到Finalization Queue中。这就出现了一个很有趣的现象,因为在Finalization Queue中的对象可以复生,如果在对象的Finalize方法中调用ReRegisterForFinalize方法,这样就形成了一个在堆上永远不会死去的对象,像凤凰涅一样每次死的时候都可以复生。
托管资源:
Net中的所有类型都是(直接或间接)从System.Object类型派生的。
CTS中的类型被分成两大类——引用类型(reference type,又叫托管类型[managed type]),分配在内存堆上,值类型(value type)。值类型分配在堆栈上。如图
值类型在栈里,先进后出,值类型变量的生命有先后顺序,这个确保了值类型变量在推出作用域以前会释放资源。比引用类型更简单和高效。堆栈是从高地址往低地址分配内存。
引用类型分配在托管堆(Managed Heap)上,声明一个变量在栈上保存,当使用new创建对象时,会把对象的地址存储在这个变量里。托管堆相反,从低地址往高地址分配内存,如图
.net中超过80%的资源都是托管资源。
非托管资源: ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,Image,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip ,文件句柄,GDI资源,数据库连接等等资源。可能在使用的时候很多都没有注意到!
.NET的GC机制有这样两个问题:
首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。
第二,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。
GC并不是实时性的,这会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托管资源。使用using 语句可以简化资源管理。
示例
名称
说明
Collect()
强制对所有代进行即时垃圾回收。
Collect(Int32)
强制对零代到指定代进行即时垃圾回收。
Collect(Int32, GCCollectionMode)
强制在 GCCollectionMode 值所指定的时间对零代到指定代进行垃圾回收
GC注意事项:
1、只管理内存,非托管资源,如文件句柄,GDI资源,数据库连接等还需要用户去管理
2、循环引用,网状结构等的实现会变得简单。GC的标志也压缩算法能有效的检测这些关系,并将不再被引用的网状结构整体删除。
3、GC通过从程序的根对象开始遍历来检测一个对象是否可被其他对象访问,而不是用类似于COM中的引用计数方法。
4、GC在一个独立的线程中运行来删除不再被引用的内存
5、GC每次运行时会压缩托管堆
6、你必须对非托管资源的释放负责。可以通过在类型中定义Finalizer来保证资源得到释放。
7、对象的Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间。注意并非和C++中一样在对象超出声明周期时立即执行析构函数
8、Finalizer的使用有性能上的代价。需要Finalization的对象不会立即被清除,而需要先执行Finalizer.Finalizer不是在GC执行的线程被调用。GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行所有这些Finalizer.而GC线程继续去删除其他待回收的对象。在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。
9、.NET GC使用"代"(generations)的概念来优化性能。代帮助GC更迅速的识别那些最可能成为垃圾的对象。在上次执行完垃圾回收后新创建的对象为第0代对象。经历了一次GC周期的对象为第1代对象。经历了两次或更多的GC周期的对象为第2代对象。代的作用是为了区分局部变量和需要在应用程序生存周期中一直存活的对象。大部分第0代对象是局部变量。成员变量和全局变量很快变成第1代对象并最终成为第2代对象。
10、GC对不同代的对象执行不同的检查策略以优化性能。每个GC周期都会检查第0代对象。大约1/10的GC周期检查第0代和第1代对象。大约1/100的GC周期检查所有的对象。重新思考Finalization的代价:需要Finalization的对象可能比不需要Finalization在内存中停留额外9个GC周期。如果此时它还没有被Finalize,就变成第2代对象,从而在内存中停留更长时间。
??
?
?
?
?
?
?
?
?
?
?
?
?
?
.NET 框架的垃圾回收器管理应用程序的内存分配和释放。每次您使用 new 运算符创建对象时,运行库都从托管堆为该对象分配内存。只要托管堆中有地址空间可用,运行库就会继续为新对象分配空间。但是,内存不是无限大的。最终,垃圾回收器必须执行回收以释放一些内存。垃圾回收器优化引擎根据正在进行的分配情况确定执行回收的最佳时间。当垃圾回收器执行回收时,它检查托管堆中不再被应用程序使用的对象并执行必要的操作来回收它们占用的内存。?
开发人员在内存管理方面的背景?
根据您开发背景的不同,您在内存管理方面的经验也会有所不同。在某些情况下,您可能需要让您的编程习惯来适应公共语言运行库提供的自动内存管理。?
COM 开发人员?
COM 开发人员习惯于将实现引用计数作为一个手动的内存管理技术。每次引用一个对象,计数器就递增。如果对对象的引用超出了范围,计数器就递减。当对象的引用计数达到零时,对象被终止并释放其内存。?
引用计数方案会引发许多调试错误。如果未能严格地按照引用计数的规则进行操作,对象可能被过早释放或者未引用的对象积存在内存中。循环引用也是常见的问题根源。循环引用出现在子对象引用父对象,而父对象又引用子对象时。这种情况使两个对象都不能被释放或销毁。唯一的解决方案就是让父对象和子对象都遵守一个固定的使用和销毁模式,例如总是先由父对象删除子对象。?
当使用托管语言开发应用程序时,运行库的垃圾回收器免除了对引用进行计数的需要,因此也就避免了由这种手动管理内存方案引发的错误。?
C++ 开发人员?
C++ 开发人员熟悉与手动内存管理相关的任务。在 C++ 中,当您使用 new 运算符为对象分配内存时,您必须使用 delete 运算符释放对象的内存。这可能导致多种错误,例如忘记释放对象、引起内存泄漏或试图访问已被释放的对象的内存。?
当使用 C++ 的托管扩展或其他托管语言开发应用程序时,您就不必使用 delete 运算符释放对象了。垃圾回收器在当对象不再被应用程序使用时自动为您完成这些操作。?
考虑到手动管理短期对象内存的相关成本,C++ 开发人员可能习惯于避免使用这些对象。对于两次回收间创建的然后又不再使用的托管短期对象,分配和释放内存的成本非常低。在 .NET 框架中,实际上已经对垃圾回收器进行了优化来管理具有较短生存期的对象。当开发托管应用程序时,在短期对象可以简化代码的情况下使用它们是非常合适的。?
Visual Basic 开发人员?
Visual Basic 开发人员习惯于自动内存管理。您熟悉的编程惯例将应用于您在 .NET 框架中创建的大多数托管对象。但是,当创建或使用封装非托管资源的对象时,您应该特别注意使用 Dispose 方法的推荐设计模式。?
.NET 框架支持的托管语言比此处介绍的还要多。不管您使用哪一种托管语言,.NET 框架的垃圾回收器都提供自动内存管理。它为托管对象分配和释放内存,并在必要时执行 Finalize 方法和析构函数来适当地清理非托管资源。自动内存管理通过消除手动内存管理方案引起的常见问题简化了开发。?
Finalize 方法和析构函数?
对于您的应用程序创建的大多数对象,可以依靠 .NET 框架的垃圾回收器隐式地执行所有必要的内存管理任务。但是,在您创建封装非托管资源的对象时,当您在应用程序中使用完这些非托管资源之后,您必须显式地释放它们。最常见的一类非托管资源就是包装操作系统资源的对象,例如文件、窗口或网络连接。虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它不了解具体如何清理这些资源。对于这些类型的对象,.NET 框架提供 Object.Finalize 方法,它允许对象在垃圾回收器回收该对象使用的内存时适当清理其非托管资源。默认情况下,Finalize 方法不执行任何操作。如果您要让垃圾回收器在回收对象的内存之前对对象执行清理操作,您必须在类中重写 Finalize 方法。当使用 C# 和 C++ 的托管扩展以外的编程语言进行开发时,您可以实现 Finalize 方法。C# 和托管扩展提供析构函数作为编写终止代码的简化机制。析构函数自动生成 Finalize 方法和对基类的 Finalize 方法的调用。在 C# 和托管扩展编程语言中,您必须为终止代码使用析构函数语法。?
垃圾回收器使用名为“终止队列”的内部结构跟踪具有 Finalize 方法的对象。每次您的应用程序创建具有 Finalize 方法的对象时,垃圾回收器都在终止队列中放置一个指向该对象的项。托管堆中所有需要在垃圾回收器回收其内存之前调用它们的终止代码的对象都在终止队列中含有项。?
实现 Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。这时,它不能回收具有终结器的不可访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。一个特殊的运行库线程开始处于活动状态并调用列表中对象的 Finalize 方法,然后将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。?
清理非托管资源?
通过将对象的范围限制为 protected,您可以防止应用程序的用户直接调用对象的 Finalize 方法。除此之外,我们强烈建议您不要直接从应用程序代码中调用非基类的类的 Finalize 方法。为适当处置非托管资源,建议您实现公共的 Dispose 或 Close 方法,这两个方法执行必要的对象清理代码。IDisposable 接口为实现接口的资源类提供 Dispose method。因为 Dispose 方法是公共的,所以应用程序的用户可以直接调用该方法来释放非托管资源占用的内存。如果正确实现了 Dispose 方法,则 Finalize 方法(或者 C# 中的析构函数或 C++ 的托管扩展)就成为避免在没有调用 Dispose 方法的情况下清理资源的一种防护措施。?
实现 Dispose 方法 [C#]?
类型的 Dispose 方法应该释放它拥有的所有资源。它还应该通过调用其父类型的 Dispose 方法释放其基类型拥有的所有资源。该父类型的 Dispose 方法应该释放它拥有的所有资源并同样也调用其父类型的 Dispose 方法,从而在整个基类型层次结构中传播该模式。要确保始终正确地清理资源,Dispose 方法应该可以被多次安全调用而不引发任何异常。?
Dispose 方法应该为它处置的对象调用 GC.SuppressFinalize 方法。如果对象当前在终止队列中,GC.SuppressFinalize 防止其 Finalize 方法被调用。请记住,执行 Finalize 方法会大大减损性能。如果您的 Dispose 方法已经完成了清理对象的工作,那么垃圾回收器就不必调用对象的 Finalize 方法了。?
下面的代码示例旨在阐释如何为封装了非托管资源的类实现 Dispose 方法的一种可能的设计模式。因为该模式是在整个 .NET 框架中实现的,所以您可能会发现它十分便于使用。但是,这不是 Dispose 方法唯一可能的实现。?
资源类通常是从复杂的本机类或 API 派生的,而且必须进行相应的自定义。使用这一代码模式作为创建资源类的一个起始点,并根据封装的资源提供必要的自定义。不能编译该示例,也不能将其直接用于应用程序。?
在此示例中,基类 BaseResource 实现可由类的用户调用的公共 Dispose 方法。而该方法又调用 virtual Dispose(bool disposing) 方法(Visual Basic 中的虚 Dispose(作为布尔值处置))。根据调用方的标识传递 true 或 false。以虚 Dispose 方法为对象执行适当的清理代码。?
Dispose(bool disposing) 以两种截然不同的方案执行。如果处置结果为 true,则该方法已由用户的代码直接调用或间接调用,并且可处置托管资源和非托管资源。如果处置结果为 false,则该方法已由运行库从终结器内部调用,并且只能处置非托管资源。因为终结器不会以任意特定的顺序执行,所以当对象正在执行其终止代码时,不应引用其他对象。如果正在执行的终结器引用了另一个已经终止的对象,则该正在执行的终结器将失败。?
基类提供的 Finalize 方法或析构函数在未能调用 Dispose 的情况下充当防护措施。Finalize 方法调用带有参数的 Dispose 方法,同时传递 false。不应在 Finalize 方法内重新创建 Dispose 清理代码。调用 Dispose(false) 可以优化代码的可读性和可维护性。?
类 MyResourceWrapper 阐释如何用 Dispose 从实现资源管理的类派生。MyResourceWrapper 重写 virtual Dispose(bool disposing) 方法并为其创建的托管和非托管资源提供清理代码。MyResourceWrapper 还对其基类 BaseResource 调用 Dispose 以确保其基类能够适当地进行清理。请注意,派生类 MyResourceWrapper 没有不带参数的 Finalize 方法或 Dispose 方法,因为这两个方法从基类 BaseResource 继承它们。?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
1,什么是垃圾Garbage?
每一个应用程序都有一组根root。根可以用来标志存储位置,这些位置指向托管堆中的对象或者被设为null的对象,称为活动根,并形成一个活动根列表。JIT编译器和CLR维护着活动根列表,列表能够被垃圾收集器算法访问。这样,垃圾被定义为从活动根列表中不能访问的堆上对象。
?FrankClassWithDispose?has?been?created??"?);
28??????????}?
29???????????//?测试方法?
30???????????public???void??DoSomething()
31???????????{
32??
33???????????????///?/code?here?to?do?something?
34???????????????return??;
35??????????}?
36???????????//?实现Dispose,释放本类使用的资源?
37???????????public???void??Dispose()
38???????????{
39???????????????if??(_odbcConnection??!=???null?)
40??????????????????_odbcConnection.Dispose();
41??????????????Console.WriteLine(?"?FrankClassWithDispose?has?been?disposed?"?);
42??????????}?
43??????}?
44???????//?没有实现Finalize,等着GC回收FrankClassFinalize的实例资源,GC运行时候直接回收?
45???????public???class??FrankClassNoFinalize
46???????{
47???????????private??OdbcConnection?_odbcConnection??=???null?;
48???????????//?构造函数?
49???????????public??FrankClassNoFinalize()
50???????????{
51???????????????if??(_odbcConnection??==???null?)
52??????????????????_odbcConnection??=???new??OdbcConnection();
53??????????????Console.WriteLine(?"?FrankClassNoFinalize??has?been?created?"?);
54??????????}?
55???????????//?测试方法?
56???????????public???void??DoSomething()
57???????????{
58??
59???????????????//?GC.Collect();?
60???????????????///?/code?here?to?do?something?
61???????????????return??;
62??????????}?
63??????}?
64???????//?实现析构函数,编译为Finalize方法,调用对象的析构函数
65???????//?GC运行时,两次调用,第一次没释放资源,第二次才释放
66???????//?FrankClassDestructor的实例资源
67???????//?CLR使用独立的线程来执行对象的Finalize方法,频繁调用会使性能下降?
68???????public???class??FrankClassWithDestructor
69???????{
70???????????private??OdbcConnection?_odbcConnection??=???null?;
71???????????//?构造函数?
72???????????public??FrankClassWithDestructor()
73???????????{
74???????????????if??(_odbcConnection??==???null?)
75??????????????????_odbcConnection??=???new??OdbcConnection();
76??????????????Console.WriteLine(?"?FrankClassWithDestructor??has?been?created?"?);
77??????????}?
78???????????//?测试方法?
79???????????public???void??DoSomething()
80???????????{
81???????????????///?/code?here?to?do?something?
82??
83???????????????return??;
84??????????}?
85???????????//?析构函数,释放未托管资源?
86???????????~?FrankClassWithDestructor()
87???????????{
88???????????????if??(_odbcConnection??!=???null?)
89??????????????????_odbcConnection.Dispose();
90??????????????Console.WriteLine(?"?FrankClassWithDestructor??has?been?disposed?"?);
91??????????}?
92??????}?
93??}?
94?
其中使用了非托管的对象OdbcConnection?的实例。建立的客户端进行了简单的测试。客户端代码如下:
?
?
??有些时候资源必须在特定时间释放,类可以实现执行资源管理和清除任务方法IDisposable.Dispose的接口IDisposable。
如果调用者需要调用Dispose方法清理对象,类作为契约的一部分必须实现Dispose方法。垃圾收集器默认情况下不会调用
Dispose方法;然而,实现Dispose方法可以调用GC里的方法去规范垃圾收器的终结行为。
值得一提的是:调用Dispose()方法,主动释放资源,灵活,可以使用Using语句创建非托管对象,方法执行结束前,会调用
Dispose()方法释放资源,?这两端代码的效果是一样的,可以查看编译后IL。
Using 语句有同样的效果,来实现非托管对象资源的释放。这点在面试中也会经常遇到,Using关键字的用法有哪几种等等类似的问题。基本理想的答案都是除了引用命名空间,和命名空间设置别名外,就是这个用法实现如try finally块一样作用的对非托管对象资源的回收。只是一种简便的写法。
???? 当你用Dispose方法释放未托管对象的时候,应该调用GC.SuppressFinalize。如果对象正在终结队列(finalization queue),GC.SuppressFinalize会阻止GC调用Finalize方法。因为Finalize方法的调用会牺牲部分性能。如果你的Dispose方法已经对委托管资源作了清理,就没必要让GC再调用对象的Finalize方法(MSDN)。附上MSDN的代码,?大家可以?参考.
?
?public???class??BaseResource:?IDisposable
?{
?????//??指向外部非托管资源?
?????private??IntPtr?handle;
?????//??此类使用的其它托管资源.?
?????private??Component?Components;
?????//??跟踪是否调用.Dispose方法,标识位,控制垃圾收集器的行为?
?????private???bool??disposed??=???false?;
?
?????//??构造函数?
?????public??BaseResource()
?????{
????????//??Insert?appropriate?constructor?code?here.?
????}?
?
?????//??实现接口IDisposable.
?????//??不能声明为虚方法virtual.
?????//??子类不能重写这个方法.?
?????public???void??Dispose()
?????{
???????Dispose(?true?);
????????//??离开终结队列Finalization?queue?
????????//??设置对象的阻止终结器代码
????????//???
???????GC.SuppressFinalize(?this?);
????}?
?
?????//??Dispose(bool?disposing)?执行分两种不同的情况.
?????//??如果disposing?等于?true,?方法已经被调用
?????//??或者间接被用户代码调用.?托管和非托管的代码都能被释放
?????//??如果disposing?等于false,?方法已经被终结器?finalizer?从内部调用过,
?????//?你就不能在引用其他对象,只有非托管资源可以被释放。?
?????protected???virtual???void??Dispose(?bool??disposing)
?????{
????????//??检查Dispose?是否被调用过.?
????????if?(?!?this?.disposed)
????????{
???????????//??如果等于true,?释放所有托管和非托管资源??
???????????if?(disposing)
???????????{
??????????????//??释放托管资源.?
?????????????Components.Dispose();
??????????}?
???????????//??释放非托管资源,如果disposing为?false,?
???????????//??只会执行下面的代码.?
??????????CloseHandle(handle);
??????????handle??=??IntPtr.Zero;
???????????//??注意这里是非线程安全的.
???????????//??在托管资源释放以后可以启动其它线程销毁对象,
???????????//??但是在disposed标记设置为true前
???????????//??如果线程安全是必须的,客户端必须实现。?
?
???????}?
???????disposed??=???true?;?????????
????}?
??????????//??使用interop?调用方法?
??????????//??清除非托管资源.?
?????????[System.Runtime.InteropServices.DllImport(?"?Kernel32?"?)]
??????????private???extern???static??Boolean?CloseHandle(IntPtr?handle);
?
?????//??使用C#?析构函数来实现终结器代码
?????//??这个只在Dispose方法没被调用的前提下,才能调用执行。
?????//??如果你给基类终结的机会.
?????//??不要给子类提供析构函数.?
?????~?BaseResource()??????
?????{
????????//??不要重复创建清理的代码.
????????//??基于可靠性和可维护性考虑,调用Dispose(false)?是最佳的方式?
???????Dispose(?false?);
????}?
?
?????//??允许你多次调用Dispose方法,
?????//??但是会抛出异常如果对象已经释放。
?????//??不论你什么时间处理对象都会核查对象的是否释放,?
?????//??check?to?see?if?it?has?been?disposed.?
?????public???void??DoSomething()
?????{
????????if?(?this?.disposed)
????????{
???????????throw???new??ObjectDisposedException();
???????}?
????}?
????对于需要调用Close方法比Dispose方法更加自然的类型,可以在??基类增加一个Close方法。
???Close方法无参调用执行恰当清理工作的Dispose方法。
???下面的例子演示了Close方法。
?????//??不要设置方法为virtual.
?????//??继承类不允许重写这个方法?
?????public???void??Close()
?????{
?????//??无参数调用Dispose参数.?
????????Dispose();
????}?
?
?????public???static???void??Main()
?????{
??????????//??Insert?code?here?to?create
??????????//??and?use?a?BaseResource?object.?
????}?
?}?
图1托管堆
这时候,NextObjPtr指向托管堆上下一个对象分配的位置,图1显示一个托管堆中有三个对象A、B和C。下一个对象会放在NextObjPtr指向的位置(紧挨着C对象)
现在让我们再看一下c-runtime堆如何分配内存。在c-runtime堆,分配内存需要遍历一个链表的数据结构,直到找到一个足够大的内存块,这个内存块有可能会被拆分,拆分后链表中的指针要指向剩余内存空间,要确保链表的完好。对于托管堆,分配一个对象只是修改NextObjPtr指针的指向,这个速度是非常快的。事实上,在托管堆上分配一个对象和在线程栈上分配内存的速度很接近。
到目前为止,托管堆上分配内存的速度似乎比在c-runtime堆上的更快,实现上也更简单一些。当然,托管堆获得这个优势是因为做了一个假设:地址空间是无限的。很显然这个假设是错误的。必须有一种机制保证这个假设成立。这个机制就是垃圾回收器。让我们看下它如何工作。
当应用程序调用new操作符创建对象时,有可能已经没有内存来存放这个对象了。托管堆可以检测到NextObjPtr指向的空间是否超过了堆的大小,如果超过了就说明托管堆满了,就需要做一次垃圾回收了。
在现实中,在0代堆满了之后就会触发一次垃圾回收。“代”是垃圾回收器提升性能的一种实现机制。“代”的意思是:新创建的对象是年轻一代,而在回收操作发生之前没有被回收掉的对象是较老的对象。将对象分成几代可以允许垃圾回收器只回收某一代的对象,而不是回收所有对象。
垃圾回收算法:
垃圾回收器检查看是否存在应用程序不再使用的对象。如果这样的对象存在,那么这些对象占用的空间就可以被回收(如果堆上没有足够的内存可用,那么new操作符就会抛出OutofMemoryException)。你可能会问垃圾回收器是怎样判断一个对象是否还在用呢?这个问题不太容易得到答案。
每个应用程序都有一组根对象,根是一些存储位置,他们可能指向托管堆上的某个地址,也可能是null。例如,所有的全局和静态对象指针是应用程序的根对象,另外在线程栈上的局部变量/参数也是应用程序的根对象,还有CPU寄存器中的指向托管堆的对象也是根对象。存活的根对象列表由JIT(just-in-time)编译器和clr维护,垃圾回收器可以访问这些根对象的。
当垃圾回收器开始运行,它会假设托管堆上的所有对象都是垃圾。也就是说,假定没有根对象,也没有根对象引用的对象。然后垃圾回收器开始遍历根对象并构建一个由所有和根对象之间有引用关系对象构成的图。
图2显示,托管堆上应用程序的根对象是A,C,D和F,这几个对象就是图的一部分,然后对象D引用了对象H,那么对象H也被添加到图中;垃圾回收器会循环遍历所有可达对象。
图2 托管堆上的对象
垃圾回收器会挨个遍历根对象和引用对象。如果垃圾回收器发现一个对象已经在图中就会换一个路径继续遍历。这样做有两个目的:一是提高性能,二是避免无限循环。
所有的根对象都检查完之后,垃圾回收器的图中就有了应用程序中所有的可达对象。托管堆上所有不在这个图上的对象就是要做回收的垃圾对象了。构建好可达对象图之后垃圾回收器开始线性的遍历托管堆,找到连续垃圾对象块(可以认为是空闲内存)。然后垃圾回收器将非垃圾对象移动到一起(使用c语言中的memcpy函数),覆盖所有的内存碎片。当然,移动对象时要禁用所有对象的指针(因为他们都可能是错误的了)。因此垃圾回收器必须修改应用程序的根对象使他们指向对象的新内存地址。此外,如果某个对象包含另一个对象的指针,垃圾回收器也要负责修改引用。图3显示了一次回收之后的托管堆。
图3 回收之后的托管堆
如图3所示在回收之后,所有的垃圾对象都被标识出来,而所有的非垃圾对象被移动到一起。所有的非垃圾对象的指针也被修改成移动后的内存地址,NextObjPtr指向最后一个非垃圾对象的后面。这时候new操作符就可以继续成功的创建对象了。
如你看到的,垃圾回收会有显著的性能损失,这是使用托管堆的一个明显的缺点。 不过,要记着内存回收操作旨在托管堆慢了之后才会执行。在满之前托管堆的性能比c-runtime堆的性能好要好。运行时垃圾回收器还会做一些性能优化,我们在下一篇文章中谈论这个。
下面的代码说明了对象是如何被创建管理的:
Finalize方法要做的事情通常是回收垃圾回收器不能回收的资源,例如文件句柄,数据库连接等等。
当垃圾回收时,对象B、E、G、H、I和J被标记为垃圾。垃圾回收器扫描终结队列找到这些对象的指针。当发现对象指针时,指针会被移动到Freachable队列。Freachable队列是另一个由垃圾回收器控制的内部数据结构。在Freachable队列中的每一个对象的Finalize方法将执行。
垃圾回收之后,托管堆如图6所示。你可以看到对象B、G、H已经被回收了,因为这几个对象没有Finalize方法。然而对象E、I、J还没有被回收掉,因为他们的Finalize方法还没有执行。
图5 垃圾回收后的托管堆
程序运行时会有一个专门的线程负责调用Freachable队列中对象的Finalize方法。当Freachable队列为空时,这个线程会休眠,当队列中有对象时,线程被唤醒,移除队列中的对象,并调用它们的Finalize方法。因此在执行Finalize方法时不要企图访问线程的local storage。
终结队列(finalization queue)和Freachable队列之间的交互很巧妙。首先让我告诉你freachable的名字是怎么来的。F显然是finalization;在此队列中的每一个对象都在等待执行他们的Finalize方法;reachable意思是这些对象来了。另一种说法,Freachable队列中的对象被认为是跟对象,就像是全局变量或静态变量。因此,如果一个对象在freachable队列中,那么这个对象就不是垃圾。
简短点说,当一个对象是不可达的,垃圾回收器会认为这个对象是垃圾。那么,当垃圾回收器将对象从终结队列移动到Freachable队列中,这些对象就不再是垃圾了,它们的内存也不会回收。从这一点上来讲,垃圾回收器已经完成标识垃圾,一些对象被标识成垃圾又被重新认为成非垃圾对象。垃圾回收器回收压缩内存,清空freachable队列,执行队列中每一个对象的Finalize方法。
图6 再次执行垃圾回收后的托管堆
再次出发垃圾回收之后,实现Finalize方法的对象才被真正的回收。这些对象的Finalize方法已经执行过了,Freachable队列清空了。
垃圾回收让对象复活
在前面部分我们已经说了,当程序不使用某个对象时,这个对象会被回收。然而,如果对象实现了Finalize方法,只有当对象的Finalize方法执行之后才会认为这个对象是可回收对象并真正回收其内存。换句话说,这类对象会先被标识为垃圾,然后放到freachable队列中复活,然后执行Finalize之后才被回收。正是Finalize方法的调用,让这种对象有机会复活,我们可以在Finalize方法中让某个对象强引用这个对象;那么垃圾回收器就认为这个对象不再是垃圾了,对象就复活了。
如下复活演示代码:
????????????????当C#编译器遇到new关键字时,它会在方法的实现中加入一条CIL newObj指令。CIL newObj指令的核心任务有:
????????a.计算分配对象所需要的总内存数(包含类型的成员变量和类型的基类所需的必要内存);
????????b.检查托管堆,确保有足够的空间来放置要分配的对象。如果空间足够,调用类型的构造函数,最终将内存中新对象的引用返回给调用者;
????????c.在将引用返回给调用者之前,移动下一个对象的指针(将分配的新对象的指针),指向托管堆上的下一个可用的位置。
????????该基本过程如下图所示:
???
????????当处理newObj指令时,如果CLR判定托管堆没有足够的空间来分配所请求的类型,它会执行一次垃圾回收来尝试释放内存。
???????对象创建后,对于值类型的对象,当它们越出定义的作用域时(如:方法内的临时变量,在方法返回后),这个类型的对象就消亡了;对于引用类型的对象,它们分配在托管堆上,这些对象一直保留在内存中,直到.NET垃圾回收器将它们销毁。
?
垃圾回收
?
????????如前所述,引用类型的对象由new创建后,垃圾回收器将在不再需要时才将其销毁。问题是垃圾回收器如何判断一个对象什么时候不再需要呢?简单的说是,当一个对象从代码库的任何部分都不可达(即对象没有根)时,垃圾回收器就将其标记为垃圾,成为垃圾回收的候选目标。
????????为了优化CLR寻找不可达对象的过程,堆上的每一个对象被指定为属于某“代(generation)”,代的思路很简单:对象在堆上存在的时间越长,它就更应该保留(如实现Main方法的对象,应用程序对象),相反最近才放在堆上的对象可能很快就不可达了(如在方法作用域中创建的对象,本地变量)。基于此,每一个堆上的对象都属于下列某代:
????????第0代:从没有被标记为回收的新分配的对象
????????第1代:在上一次垃圾回收中没有被回收的对象(即它被标记为回收,但因为已经获取了足够的堆空间而没有被回收)
????????第2代:在一次以上的垃圾回收后仍然没有被回收的对象
????????垃圾回收时,垃圾回收器首先处理第0代对象,如果不够再处理第1代、甚至第2代对象,剩余的第0代对象提升为第1代,以此类推,但上限为第2代。
????????当确实发生回收时,垃圾回收器暂时挂起所有再当前进程中所有活动的线程,以保证应用程序在回收过程中不会访问堆,一旦垃圾回收周期完成,就允许挂起的线程继续它们的工作。
?
构建可终结类型
?
????????在System.Object类中,定义了Finalize()虚方法,该方法的作用是保证.NET对象能在垃圾回收时清除非托管资源。非托管资源(如原始的文件句柄、数据库连接等)是通过使用PInvoke(平台调用)服务直接调用操作系统的API,或通过一些复杂的COM交互获得的。显然该方法的默认实现什么也不做。
????????由于有垃圾回收,大多数C#类都不需要重写Finalize()方法来显式的指定清理逻辑,一切托管对象最终都会被垃圾回收。只是在使用非托管资源时,才可能需要自定义清理逻辑。Finalize()方法是受保护的,所以不可能直接调用一个对象的Finalize()方法,在从内存回收这个对象之前,垃圾回收器会自动调用对象的Finalize()方法(如果支持的话),细节见后面的描述。
?
????????注:在结构类型上重写Finalize()是不合法的,因为结构是值类型,它们本来就从不分配在堆上
????????
????????在C#中重写Finalize()方法比较奇怪,不能用预期的override关键字来做:
???????? }
?
??????? ~DisposeClass()
??????? {
??????????? Debug.WriteLine( "Destructor called!" );
??????? }
?
??????? #region IDisposable Members
?
??????? public void Dispose()
??????? {
??????????? // TODO:? Add DisposeClass.Dispose implementation
??????????? Debug.WriteLine( "Dispose called!" );
??????? }
?
??????? #endregion
??? }
?
对于Close来说不属于真正意义上的释放,除了注意它需要显示被调用外,我在此对它不多说了。而对于析构函数而言,不是在对象离开作用域后立刻被执行,只有在关闭进程或者调用GC.Collect方法的时候才被调用,参看如下的代码运行结果。
??????? private void Create()
??????? {
??????????? DisposeClass myClass = new DisposeClass();
??????? }
?
??????? private void CallGC()
??????? {
??????????? GC.Collect();
??????? }
?
??????? // Show destructor
??????? Create();
??????? Debug.WriteLine( "After created!" );
??????? CallGC();
?
运行的结果为:
After created!
Destructor called!
?
显然在出了Create函数外,myClass对象的析构函数没有被立刻调用,而是等显示调用GC.Collect才被调用。
?
对于Dispose来说,也需要显示的调用,但是对于继承了IDisposable的类型对象可以使用using这个关键字,这样对象的Dispose方法在出了using范围后会被自动调用。例如:
??? using( DisposeClass myClass = new DisposeClass() )
??? {
??????? //other operation here
??? }
?
如上运行的结果如下:
Dispose called!
?
那么对于如上DisposeClass类型的Dispose实现来说,事实上GC还需要调用对象的析构函数,按照前面的GC流程来说,GC对于需要调用析构函数的对象来说,至少经过两个步骤,即首先调用对象的析构函数,其次回收内存。也就是说,按照上面所写的Dispose函数,虽说被执行了,但是GC还是需要执行析构函数,那么一个完整的Dispose函数,应该通过调用GC.SuppressFinalize(this )来告诉GC,让它不用再调用对象的析构函数中。那么改写后的DisposeClass如下:
??? /// <summary>
??? /// The class to show three disposal function
??? /// </summary>
??? public class DisposeClass:IDisposable
??? {
??????? public void Close()
??????? {
??????????? Debug.WriteLine( "Close called!" );
??????? }
?
??????? ~DisposeClass()
??????? {
??????????? Debug.WriteLine( "Destructor called!" );
??????? }
?
??????? #region IDisposable Members
?
??????? public void Dispose()
??????? {
??????????? // TODO:? Add DisposeClass.Dispose implementation
??????????? Debug.WriteLine( "Dispose called!" );
??????????? GC.SuppressFinalize( this );
??????? }
?
??????? #endregion
??? }
?
通过如下的代码进行测试。
??????? private void Run()
??????? {
??????????? using( DisposeClass myClass = new DisposeClass() )
??????????? {
??????????????? //other operation here
??????????? }
??????? }
?
??????? private void CallGC()
??????? {
??????????? GC.Collect();
??? ??? }
?
??????? // Show destructor
??????? Run();
??????? Debug.WriteLine( "After Run!" );
??????? CallGC();
?
运行的结果如下:
Dispose called!
After Run!
?
显然对象的析构函数没有被调用。通过如上的实验以及文字说明,大家会得到如下的一个对比表格。
?
析构函数
Dispose方法
Close方法
意义
销毁对象
销毁对象
关闭对象资源
调用方式
不能被显示调用,会被GC调用
需要显示调用
或者通过using语句
需要显示调用
调用时机
不确定
确定,在显示调用或者离开using程序块
确定,在显示调用时
?
那么在定义一个类型的时候,是否一定要给出这三个函数地实现呢。
?
我的建议大致如下。
<!--[if !supportLists]-->1.?<!--[endif]-->提供析构函数,避免资源未被释放,主要是指非内存资源;
<!--[if !supportLists]-->2.?<!--[endif]-->对于Dispose和Close方法来说,需要看所定义的类型所使用的资源(参看前面所说),而决定是否去定义这两个函数;
<!--[if !supportLists]-->3.?<!--[endif]-->在实现Dispose方法的时候,一定要加上“GC.SuppressFinalize( this )”语句,避免再让GC调用对象的析构函数。
?
图1托管堆
这时候,NextObjPtr指向托管堆上下一个对象分配的位置,图1显示一个托管堆中有三个对象A、B和C。下一个对象会放在NextObjPtr指向的位置(紧挨着C对象)
现在让我们再看一下c-runtime堆如何分配内存。在c-runtime堆,分配内存需要遍历一个链表的数据结构,直到找到一个足够大的内存块,这个内存块有可能会被拆分,拆分后链表中的指针要指向剩余内存空间,要确保链表的完好。对于托管堆,分配一个对象只是修改NextObjPtr指针的指向,这个速度是非常快的。事实上,在托管堆上分配一个对象和在线程栈上分配内存的速度很接近。
到目前为止,托管堆上分配内存的速度似乎比在c-runtime堆上的更快,实现上也更简单一些。当然,托管堆获得这个优势是因为做了一个假设:地址空间是无限的。很显然这个假设是错误的。必须有一种机制保证这个假设成立。这个机制就是垃圾回收器。让我们看下它如何工作。
当应用程序调用new操作符创建对象时,有可能已经没有内存来存放这个对象了。托管堆可以检测到NextObjPtr指向的空间是否超过了堆的大小,如果超过了就说明托管堆满了,就需要做一次垃圾回收了。
在现实中,在0代堆满了之后就会触发一次垃圾回收。“代”是垃圾回收器提升性能的一种实现机制。“代”的意思是:新创建的对象是年轻一代,而在回收操作发生之前没有被回收掉的对象是较老的对象。将对象分成几代可以允许垃圾回收器只回收某一代的对象,而不是回收所有对象。
垃圾回收算法:
垃圾回收器检查看是否存在应用程序不再使用的对象。如果这样的对象存在,那么这些对象占用的空间就可以被回收(如果堆上没有足够的内存可用,那么new操作符就会抛出OutofMemoryException)。你可能会问垃圾回收器是怎样判断一个对象是否还在用呢?这个问题不太容易得到答案。
每个应用程序都有一组根对象,根是一些存储位置,他们可能指向托管堆上的某个地址,也可能是null。例如,所有的全局和静态对象指针是应用程序的根对象,另外在线程栈上的局部变量/参数也是应用程序的根对象,还有CPU寄存器中的指向托管堆的对象也是根对象。存活的根对象列表由JIT(just-in-time)编译器和clr维护,垃圾回收器可以访问这些根对象的。
当垃圾回收器开始运行,它会假设托管堆上的所有对象都是垃圾。也就是说,假定没有根对象,也没有根对象引用的对象。然后垃圾回收器开始遍历根对象并构建一个由所有和根对象之间有引用关系对象构成的图。
图2显示,托管堆上应用程序的根对象是A,C,D和F,这几个对象就是图的一部分,然后对象D引用了对象H,那么对象H也被添加到图中;垃圾回收器会循环遍历所有可达对象。
图2 托管堆上的对象
垃圾回收器会挨个遍历根对象和引用对象。如果垃圾回收器发现一个对象已经在图中就会换一个路径继续遍历。这样做有两个目的:一是提高性能,二是避免无限循环。
所有的根对象都检查完之后,垃圾回收器的图中就有了应用程序中所有的可达对象。托管堆上所有不在这个图上的对象就是要做回收的垃圾对象了。构建好可达对象图之后垃圾回收器开始线性的遍历托管堆,找到连续垃圾对象块(可以认为是空闲内存)。然后垃圾回收器将非垃圾对象移动到一起(使用c语言中的memcpy函数),覆盖所有的内存碎片。当然,移动对象时要禁用所有对象的指针(因为他们都可能是错误的了)。因此垃圾回收器必须修改应用程序的根对象使他们指向对象的新内存地址。此外,如果某个对象包含另一个对象的指针,垃圾回收器也要负责修改引用。图3显示了一次回收之后的托管堆。
图3 回收之后的托管堆
如图3所示在回收之后,所有的垃圾对象都被标识出来,而所有的非垃圾对象被移动到一起。所有的非垃圾对象的指针也被修改成移动后的内存地址,NextObjPtr指向最后一个非垃圾对象的后面。这时候new操作符就可以继续成功的创建对象了。
如你看到的,垃圾回收会有显著的性能损失,这是使用托管堆的一个明显的缺点。 不过,要记着内存回收操作旨在托管堆慢了之后才会执行。在满之前托管堆的性能比c-runtime堆的性能好要好。运行时垃圾回收器还会做一些性能优化,我们在下一篇文章中谈论这个。
下面的代码说明了对象是如何被创建管理的:
Finalize方法要做的事情通常是回收垃圾回收器不能回收的资源,例如文件句柄,数据库连接等等。
当垃圾回收时,对象B、E、G、H、I和J被标记为垃圾。垃圾回收器扫描终结队列找到这些对象的指针。当发现对象指针时,指针会被移动到Freachable队列。Freachable队列是另一个由垃圾回收器控制的内部数据结构。在Freachable队列中的每一个对象的Finalize方法将执行。
垃圾回收之后,托管堆如图6所示。你可以看到对象B、G、H已经被回收了,因为这几个对象没有Finalize方法。然而对象E、I、J还没有被回收掉,因为他们的Finalize方法还没有执行。
图5 垃圾回收后的托管堆
程序运行时会有一个专门的线程负责调用Freachable队列中对象的Finalize方法。当Freachable队列为空时,这个线程会休眠,当队列中有对象时,线程被唤醒,移除队列中的对象,并调用它们的Finalize方法。因此在执行Finalize方法时不要企图访问线程的local storage。
终结队列(finalization queue)和Freachable队列之间的交互很巧妙。首先让我告诉你freachable的名字是怎么来的。F显然是finalization;在此队列中的每一个对象都在等待执行他们的Finalize方法;reachable意思是这些对象来了。另一种说法,Freachable队列中的对象被认为是跟对象,就像是全局变量或静态变量。因此,如果一个对象在freachable队列中,那么这个对象就不是垃圾。
简短点说,当一个对象是不可达的,垃圾回收器会认为这个对象是垃圾。那么,当垃圾回收器将对象从终结队列移动到Freachable队列中,这些对象就不再是垃圾了,它们的内存也不会回收。从这一点上来讲,垃圾回收器已经完成标识垃圾,一些对象被标识成垃圾又被重新认为成非垃圾对象。垃圾回收器回收压缩内存,清空freachable队列,执行队列中每一个对象的Finalize方法。
图6 再次执行垃圾回收后的托管堆
再次出发垃圾回收之后,实现Finalize方法的对象才被真正的回收。这些对象的Finalize方法已经执行过了,Freachable队列清空了。
垃圾回收让对象复活
在前面部分我们已经说了,当程序不使用某个对象时,这个对象会被回收。然而,如果对象实现了Finalize方法,只有当对象的Finalize方法执行之后才会认为这个对象是可回收对象并真正回收其内存。换句话说,这类对象会先被标识为垃圾,然后放到freachable队列中复活,然后执行Finalize之后才被回收。正是Finalize方法的调用,让这种对象有机会复活,我们可以在Finalize方法中让某个对象强引用这个对象;那么垃圾回收器就认为这个对象不再是垃圾了,对象就复活了。
如下复活演示代码:
图1 托管堆上的0代对象
现在如果堆上添加了更多的对象,堆填满时就会触发垃圾回收。当垃圾回收器分析托管堆时,会构建一个垃圾对象(图2中浅紫色块)和非垃圾对象的图。所有没有被回收的对象会被移动压缩到堆的最底端。这些没有被回收掉的对象就成为了1代对象,如图2所示
图2 托管堆上的0代1代对象
当堆上分配了更多的对象时,新对象被放在了0代区。如果0代堆填满了,就会触发一次垃圾回收。这时候活下来的对象成为1代对象被移动到堆的底部;再此发生垃圾回收后1代对象中存活下来的对象会提升为2代对象并被移动压缩。如图3所示:
图3 托管堆上的0、1、2代对象
2代对象是目前垃圾回收器的最高代,当再次垃圾回收时,没有回收的对象的代数依然保持2.
垃圾回收分代为什么可以优化性能
如前所述,分代回收可以提高性能。当堆填满之后会触发垃圾回收,垃圾回收器可以只选择0代上的对象进行回收,而忽略更高代堆上的对象。然而,由于越年轻的对象生命周期越短,因此,回收0代堆可以回收相当多的内存,而且回收所耗的性能也比回收所有代对象要少得多。
这是分代垃圾回收的最简单优化。分代回收不需要便利整个托管堆,如果一个根对象引用了一个高代对象,那么垃圾回收器可以忽略高代对象和其引用对象的遍历,这会大大减少构建可达对象图的时间。
如果回收0代对象没有释放出足够的内存,垃圾回收器会尝试回收1代和0代堆;如果仍然没有获得足够的内存,那么垃圾回收器会尝试回收2,1,0代堆。具体会回收那一代对象的算法不是确定的,微软会持续做算法优化。
多数堆(像c-runtime堆)只要找到足够的空闲内存就分配给对象。因此,如果我连续分配多个对象时,这些对象的地址空间可能会相差几M。然而在托管堆上,连续分配的对象的内存地址是连续的。
前面的假设中还提到,新对象之间更可能存在相互引用关系。因此新对象分配到连续的内存上,你可以获得就近引用的性能优化(you gain performance from locality of reference)。这样的话很可能你的对象都在CPU的缓存中,这样CPU的很多操作就不需要去存取内存了。
微软的性能测试显示托管堆的分配速度比标准的win32 HeapAlloc方法还要快。这些测试也显示了200MHz的Pentium的CPU做一次0代回收时间可以小于1毫秒。微软的优化目的是让垃圾回收耗用的时间小于一次普通的页面错误。
使用System.GC类控制垃圾回收
类型System.GC运行开发人员直接控制垃圾回收器。你可以通过GC.MaxGeneration属性获得GC的最高代数,目前最高代是定值2.
你可以调用GC.Collect()方法强制垃圾回收器做垃圾回收,Collect方法有两个重载:
这些性能指标的具体含义如下:
性能计数器
说明
# Bytes in all Heaps(所有堆中的字节数)
显示以下计数器值的总和:“第 0 级堆大小”计数器、“第 1 级堆大小”计数器、“第 2 级堆大小”计数器和“大对象堆大小”计数器。此计数器指示在垃圾回收堆上分配的当前内存(以字节为单位)。
# GC Handles(GC?处理数目)
显示正在使用的垃圾回收处理的当前数目。垃圾回收处理是对公共语言运行库和托管环境外部的资源的处理。
# Gen 0 Collections(第?2?级回收次数)
显示自应用程序启动后第 0 级对象(即最年轻、最近分配的对象)被垃圾回收的次数。
当第 0 级中的可用内存不足以满足分配请求时发生第 0 级垃圾回收。此计数器在第 0 级垃圾回收结束时递增。较高级的垃圾回收包括所有较低级的垃圾回收。当较高级(第 1 级或第 2 级)垃圾回收发生时此计数器被显式递增。
此计数器显示最近的观察所得值。_Global_?计数器值不准确,应该忽略。
# Gen 1 Collections(第?2?级回收次数)
显示自应用程序启动后对第 1 级对象进行垃圾回收的次数。
此计数器在第 1 级垃圾回收结束时递增。较高级的垃圾回收包括所有较低级的垃圾回收。当较高级(第 2 级)垃圾回收发生时此计数器被显式递增。
此计数器显示最近的观察所得值。_Global_?计数器值不准确,应该忽略。
# Gen 2 Collections(第?2?级回收次数)
显示自应用程序启动后对第 2 级对象进行垃圾回收的次数。此计数器在第 2 级垃圾回收(也称作完整垃圾回收)结束时递增。
此计数器显示最近的观察所得值。_Global_?计数器值不准确,应该忽略。
# Induced GC(引发的?GC?的数目)
显示由于对?GC.Collect?的显式调用而执行的垃圾回收的峰值次数。让垃圾回收器对其回收的频率进行微调是切实可行的。
# of Pinned Objects(钉住的对象的数目)
显示上次垃圾回收中遇到的钉住的对象的数目。钉住的对象是垃圾回收器不能移入内存的对象。此计数器只跟踪被进行垃圾回收的堆中的钉住的对象。例如,第 0 级垃圾回收导致仅枚举第 0 级堆中钉住的对象。
# of Sink Blocks in use(正在使用的接收块的数目)
显示正在使用的同步块的当前数目。同步块是为存储同步信息分配的基于对象的数据结构。同步块保留对托管对象的弱引用并且必须由垃圾回收器扫描。同步块不局限于只存储同步信息;它们还可以存储 COM interop 元数据。该计数器指示与同步基元的过度使用有关的性能问题。
# Total committed Bytes(提交字节的总数)
显示垃圾回收器当前提交的虚拟内存量(以字节为单位)。提交的内存是在磁盘页面文件中保留的空间的物理内存。
# Total reserved Bytes(保留字节的总数)
显示垃圾回收器当前保留的虚拟内存量(以字节为单位)。保留内存是为应用程序保留(但尚未使用任何磁盘或主内存页)的虚拟内存空间。
% Time in GC(GC?中时间的百分比)
显示自上次垃圾回收周期后执行垃圾回收所用运行时间的百分比。此计数器通常指示垃圾回收器代表该应用程序为收集和压缩内存而执行的工作。只在每次垃圾回收结束时更新此计数器。此计数器不是一个平均值;它的值反映了最近观察所得值。
Allocated Bytes/second(每秒分配的字节数)
显示每秒在垃圾回收堆上分配的字节数。此计数器在每次垃圾回收结束时(而不是在每次分配时)进行更新。此计数器不是一段时间内的平均值;它显示最近两个样本中观测的值的差除以取样间隔时间所得的结果。
Finalization Survivors(完成时存留对象数目)
显示因正等待完成而从回收后保留下来的进行垃圾回收的对象的数目。如果这些对象保留对其他对象的引用,则那些对象也保留下来,但此计数器不对它们计数。“从第 0 级提升的完成内存”和“从第 1 级提升的完成内存”计数器表示因完成而保留下来的所有内存。
此计数器不是累积计数器;它在每次垃圾回收结束时由仅在该特定回收期间存留对象的计数更新。此计数器指示由于完成应用程序可能导致系统开销过高。
Gen 0 heap size(第?2?级堆大小)
显示在第 0 级中可以分配的最大字节数;它不指示在第 0 级中当前分配的字节数。
当自最近回收后的分配超出此大小时发生第 0 级垃圾回收。第 0 级大小由垃圾回收器进行微调并且可在应用程序执行期间更改。在第 0 级回收结束时,第 0 级堆的大小是 0 字节。此计数器显示调用下一个第 0 级垃圾回收的分配的大小(以字节为单位)。
此计数器在垃圾回收结束时(而不是在每次分配时)进行更新。
Gen 0 Promoted Bytes/Sec(从第?1?级提升的字节数/秒)
显示每秒从第 0 级提升到第 1 级的字节数。内存在从垃圾回收保留下来后被提升。此计数器是每秒创建的在相当长时间保留下来的对象的指示符。
此计数器显示在最后两个样本(以取样间隔持续时间来划分)中观察到的值之间的差异。
Gen 1 heap size(第?2?级堆大小)
显示第 1 级中的当前字节数;此计数器不显示第 1 级的最大大小。不直接在此代中分配对象;这些对象是从前面的第 0 级垃圾回收提升的。此计数器在垃圾回收结束时(而不是在每次分配时)进行更新。
Gen 1 Promoted Bytes/Sec(从第?1?级提升的字节数/秒)
显示每秒从第 1 级提升到第 2 级的字节数。在此计数器中不包括只因正等待完成而被提升的对象。
内存在从垃圾回收保留下来后被提升。不会从第 2 级进行任何提升,因为它是最旧的一级。此计数器是每秒创建的非常长时间保留下来的对象的指示符。
此计数器显示在最后两个样本(以取样间隔持续时间来划分)中观察到的值之间的差异。
Gen 2 heap size(第?2?级堆大小)
显示第 2 级中当前字节数。不直接在此代中分配对象;这些对象是在以前的第 1 级垃圾回收期间从第 1 级提升的。此计数器在垃圾回收结束时(而不是在每次分配时)进行更新。
Large Object Heap size(大对象堆大小)
显示大对象堆的当前大小(以字节为单位)。垃圾回收器将大于 20 KB 的对象视作大对象并且直接在特殊堆中分配大对象;它们不是通过这些级别提升的。此计数器在垃圾回收结束时(而不是在每次分配时)进行更新。
Promoted Finalization-Memory from Gen 0(从第?1?级提升的完成内存)
显示只因等待完成而从第 0 级提升到第 1 级的内存的字节数。此计数器不是累积计数器;它显示在最后一次垃圾回收结束时观察到的值。
Promoted Finalization-Memory from Gen 1(从第?1?级提升的完成内存)
显示只因等待完成而从第 1 级提升到第 2 级的内存的字节数。此计数器不是累积计数器;它显示在最后一次垃圾回收结束时观察到的值。如果最后一次垃圾回收就是第 0 级回收,此计数器则重置为 0。
Promoted Memory from Gen 0(从第?1?级提升的内存)
显示在垃圾回收后保留下来并且从第 0 级提升到第 1 级的内存的字节数。此计数器中不包括那些只因等待完成而提升的对象。此计数器不是累积计数器;它显示在最后一次垃圾回收结束时观察到的值。
Promoted Memory from Gen 1(从第?1?级提升的内存)
显示在垃圾回收后保留下来并且从第 1 级提升到第 2 级的内存的字节数。此计数器中不包括那些只因等待完成而提升的对象。此计数器不是累积计数器;它显示在最后一次垃圾回收结束时观察到的值。如果最后一次垃圾回收就是第 0 级回收,此计数器则重置为 0。
</runtime>?
</configuration>
Workstation GC – Concurrent
? Winform应用程序和Windows services 服务程序默认采用这种模式。
? 这种模式是针对交互式应用程序优化的,这种程序要求应用程序不能暂停,即使是一个相对很短暂的时间也是不行的。因为暂停进程会让用户界面闪烁或者当点击按钮的时候感觉应用程序没有响应。
? 在这种模式下,第0代和第1代的收集仍然是要暂停应用程序的线程直到收集完毕,因为第0代和第1代的收集速度很快,所以没有必要去做并行。所谓的并发是在需要第2代收集时,我们可以选择让第2代收集并行地执行或者暂停应用程序的所有线程直到收集结束。如果我们没有做选择,那么默认的就是第2代收集是并行的。
? 有一个专门的GC线程来并行地收集垃圾。在第2代收集时,用更多CPU周期和堆内存来换取更短的用户界面停顿时间。
? 并行收集仍然需要在收集过程中暂停应用程序多次,但这几次暂停相对较短,对UI线程影响较小。应用程序线程在并行收集进行中可以同时分配对象。这就要求第0代有较大的空间和较高的触发Gen 0收集的阀值。如果应用程序用完第0代的内存,而并行收集还没有进行完毕,那么应用程序不得不暂停下来,等待并行收集的完成。
如果我们要做选择,让应用程序在并行模式下运行,可以在应用程序的config文件中写上:
<configuration>
<runtime>
<gcServer enabled=“true"/>
</runtime>
</configuration>
注: Jeffrey Richter似乎在其书<<CLR via c#>>中说concurrent GC只有在多CPU机器上有。但是经过Mark的调查,实际上concurrent GC在单CPU上也有。?
这种模式与Server GC类似,推荐为那种运行在单个cpu机器上服务类型的应用程序使用。与Server GC不同的是:其收集线程即等同于应用程序线程。这是这种模式的工作流程:
1) 应用程序线程在堆上分配对象
2) 第0代没有足够空间It
3) 在同一线程上引发垃圾收集
4) 垃圾收集器调用SuspendEE函数暂停应用程序线程
5) GC做收集工作
6) GC调用RestartEE来继续应用程序的线程GC
7) 应用程序的线程继续运行
可以修改应用程序的配置来把 concurrency 关闭。
?<configuration>?
<runtime>?
?? <gcConcurrent enabled="false" />?
</runtime>?
</configuration>
下面是三种模式的对比表:
??
?
以上我们说到了CLR 4.0以前垃圾收集的大部分方面。 掌握前面的知识有助于理解CLR 4.0所带来的变化。
?
CLR 2.0以后GC有哪些变化呢?CLR V4.0带来的后台垃圾收集
? 前面我们已经提到已经有了并发垃圾收集。后台垃圾收集是来替代并发垃圾收集。后台垃圾收集是并发垃圾收集的进化。
? 当应用程序的活动量不大(包括分配和修改引用)并且堆不太大时,并发垃圾收集工作得好。由并发垃圾收集带来的延迟是可接受的。但是当人们编写更大的应用程序,需要用到更大的堆,这时由并发收集带来的延迟就变得不可接受。
? 后台垃圾收集的显著改进是:在后台垃圾收集进行中,如果需要的话,我们可以做短的垃圾收集(第0代和第1代收集)。与并发垃圾收集类似,后台垃圾收集也是只有在全部垃圾收集时才有。并且短的垃圾收集(第0代和第1代收集)总是按阻断方式进行垃圾收集(暂停应用程序)。后台垃圾收集在一个专门的线程上运行。那些与后台垃圾收集同时进行的短垃圾收集(第0代和第1代收集)被称为前台垃圾收集。
?
? 于是,当后台垃圾收集正进行中而第0代空间不足,就会触发一个第0代垃圾收集(有可能升级成第1代收集,取决于垃圾收集器的内部决策)。后台垃圾收集线程会经常检查安全点(比如前台垃圾收集发生)。当有前台垃圾收集时,后台垃圾收集会自己暂停并让前台垃圾收集可以开始开始工作。在前台垃圾收集完成之后,后台垃圾时线程和应用程序的线程就继续它们的工作。
?
? 后台垃圾收集不仅在较低的代将无用的对象清除,同时它提高了对象停留在短生命周期段的限制--如果后台垃圾收集进行中需要扩展堆,我们可以在第1代收集时这样做。而这在CLR1.0和CLR2.0只有在第2代收集时才会发生。
? 后台垃圾收集比并发收集的好处体现在:在后台收集进行中,应用程序耗尽了第0代的空间,如果是并发收集,应用程序必须暂停以等待并发收集完成。而现在不同了,可以用前台收集对第0代或者第1代进行收集。于是就不必等待。同时,如果需要,可以在第1代收集就扩展堆(增加内存段),而不必等到第2代收集。这减少了让UI应用程序暂停的机会。
? 我们通过并行地做更多事情改进了后台垃圾收集的性能,所以需要暂停应用程序线程的时间更短。我们现在没有在CLRV4.0中为Server GC提供后台垃圾收集。那将是后续的工作。
?
.net 3.5 SP1中的全部收集通知机制
??全部收集通知机制是为担负大量请求的服务器应用准备的。因为存在一些情况,CLR的全部收集会带来较大的延迟,从而影响服务器应用的性能,导致用户请求超时。针对这种情况,全部收集通知机制应运而生。当一个全垃圾收集将要开始时,你会得到通知并可以采取相应动作,如将请求导向别的服务器。
.net 3.5 SP1的GC类增加了几个方法:
GC.RegisterForFullGCNotification
GC.WaitForFullGCApproach
GC.WaitForFullGCComplete
GC.CancelFullGCNotification
并在System命名空间下增加了一个枚举
GCNotificationStatus
? 这个GC.RegisterForFullGCNotification 用来注册当前应用将要接收全部垃圾收集的通知。当一个全部垃圾收集将要发生时,注册了的应用程序就可收到通知。同时,当一个全部垃圾收集完成时,注册了的程序也会收到通知。这是GC.RegisterForFullGCNotification 的申明方式:
[PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]
public static void RegisterForFullGCNotification(
??? int maxGenerationThreshold,
??? int largeObjectHeapThreshold
)
maxGenerationThreshold是1-99之间的数.指的是第2代中存活的对象个数达到这个阀值就要通知. largeObjectHeapThreshold也是1-99之间的数.指的是大对象堆中分配的对象个数达到这个阀值就要通知.
WaitForFullGCApproach用来获得垃圾收集器是否将要进行全部垃圾收集. 它的声明是这样的:
[PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]
public static GCNotificationStatus WaitForFullGCApproach()或者[PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]
public static GCNotificationStatus WaitForFullGCApproach(
??? int millisecondsTimeout
)
第2个调用方式可以带一个等待时间, 单位是毫秒. 其返回的是枚举GCNotificationStatus, 如果是GCNotificationStatus.Succeeded, 那么我们就可以将请求导向其他服务器.与WaitForFullGCApproach类似的, WaitForFullGCComplete用来获得垃圾收集器是否已经完成全部垃圾收集, 它的声明是这样的:
[PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]
public static GCNotificationStatus WaitForFullGCComplete()或者
[PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]
public static GCNotificationStatus WaitForFullGCComplete(
??? int millisecondsTimeout
)
第2个调用方式可以带一个等待时间, 单位是毫秒. 其返回的是枚举GCNotificationStatus, 如果是GCNotificationStatus.Succeeded, 那么我们就可以将此服务器重新声明可以接受外界的请求.
WaitForFullGCApproach和WaitForFullGCComplete设计时就是要一起使用的。如果只使用一个而不使用另一个就会导致不可知的结果。
GC.CancelFullGCNotification是用来将已经注册的垃圾收集通知取消. 与GC.RegisterForFullGCNotification 一起成对配合使用.
通常的使用方法是这样的:
1.首先在主线程调用GC.RegisterForFullGCNotification 来准备收获通知
2.启动一个值守线程, 在值守线程中调用WaitForFullGCApproach和WaitForFullGCComplete来获取当前GC状态, 如果是将要进行全部垃圾收集,就做相应处理,如将请求导向其他服务器。如果是全部垃圾收集完毕,则可做相应处理, 如将此服务器标记为可接受请求.值守线程中需要不断循环来轮循GC状态.
3.当主线程将要结束, 调用GC.CancelFullGCNotification并发消息通知值守线程, 值守线程退出循环,结束.
4.主线程退出
对于asp.net应用, 可以在Application_Start中调用GC.RegisterForFullGCNotification并启动一个值守线程. 同时可以在Application_End中通知值守线程结束.
全部垃圾收集通知机制的典型用途就是在多服务器的场景.用NLB(Network load balance)搭建的Web Farm或者硬件load balance的多服务器场景. 一个比较复杂的场景是每个服务器上还有web garden设置.
注意事项: 全部垃圾收集通知机制只有在非并发模式时才可用. WinForm和Console应用都是默认为并发模式. 例如msdn上的那个例子程序,就需要在程序的app.config中加上
<configuration>?
<runtime>?
?? <gcConcurrent enabled="false" />?
</runtime>?
</configuration>
?? ?使用finally块是可行的,但由于它存在几个缺点,所以并不是一个十分理想的方案:
?? ?1)如果必须摧毁多个资源,局面很快就会变得难以控制(最终将获得大量嵌套的try和finally块)
?? ?2)它不能创建解决方案的一个抽象
?? ?3)对资源的引用保留在finally块之后的作用域中
三、using语句(推荐方案)
?? ?using语句提供了一个脉络清晰的机制来控制资源的生存期。可以创建一个对象,这个对象会在using语句块结束时销毁。
?? ?在using语句中声明的变量必须是实现了IDisposable接口的一个类型。IDisposable接口在System命名空间中,只包含一个名为Dispose方法。using语句具有以下功能:
?? ?1)需要dispose多个资源时,具有良好的扩展性
?? ?2)不影响程序代码的逻辑
?? ?3)对问题进行良好的抽象,避免重复性编码
?? ?4)非常健壮,using语句结束后,就不能使用using语句中声明的变量,因为其已经不在作用域中。如果坚持使用,会产生编译时错误。
四、从析构器中调用Dispose方法
?? ?1)类实现了IDisposable接口
?? ?2)析构器调用Dispose
?? ?3)Dispose方法是public方法,可以在任何时候调用
?? ?4)Dispose方法可以安全地多次调用
?? ?5)Dispose方法调用静态方法GC.SuppressFinalize
?? ?6)类的所有常规方法都要检查对象是否disposed;如果是,就抛出一个异常。
五、强制垃圾回收
?? ?可以调用静态方法System.GC.Collect,从而在一个程序中调用垃圾回收器。System.GC.Collect方法将启动垃圾回收器,但具体的回收过程是异步进行的。当方法结束时,程序员仍然不知道对象是否已经摧毁。