标签:
本章将讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存。简单的说,本章要解释CLR中的垃圾回收器是如何工作的,还要解释与它有关的性能问题。
在.NET Framework中,内存中的资源(即所有二进制信息的集合)分为“托管资源”和“非托管资源”。托管资源必须接受.NET Framework的CLR的管理 (如内存类型安全性检查) 。而非托管资源则不必接受.NET Framework的CLR管理, 需要手动清理垃圾(显式释放)。注意,“垃圾回收”机制是.NET Framework的特性,而不是C#的。
每个程序都要使用这样或那样的资源,比如文件、内存缓冲区、屏幕空间、网络连接、数据库资源等。事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存。
以下是访问一个资源所需的具体步骤:
垃圾回收(garbage collection)自动发现和回收不再使用的内存,不需要程序员的协助。使开发人员得到了解放,现在不必跟踪内存的使用,也不必知道在什么时候释放内存。但是,垃圾回收器不可以管理内存中的所有资源,对内存中的类型所代表的资源也是一无所知的。这意味着垃圾回收器不知道怎么执行“摧毁资源的状态以进行清理”。这部分资源就需要开发人员自己写代码实现回收。在.Net framework中,开发人员通常会把清理这类资源的代码写到Dispose,Finalize和Close方法中。
在.net中提供三种模式来回收内存资源:dispose模式,finalize方法,close方法:
然而,值类型、集合类型、String、Attribute、Delegate和Exception所代表的资源无需执行特殊的清理操作。列如,只需销毁对象的内存中维护的字符数组,一个String资源就会被完全清理。
值类型(包括引用和对象实例)和引用类型的引用其实是不需要什么“垃圾回收器”来释放内存的,因为当它们出了作用域后会自动释放所占内存(因为它们都保存在“堆栈”中,学过数据结构可知这是一种先进后出的结构)。只有引用类型的引用所指向的对象实例才保存在“堆”中,而堆因为是一个自由存储空间,所以它并没有像“堆栈”那样有生存期 (“堆栈”的元素弹出后就代 表生存期结束,也就代表释放了内存)。并且非常要注意的是,“垃圾回收器”只对“堆”这块区域起作用。
从托管堆分配资源
.Net clr把所有的引用对象都分配到托管堆上,这一点很像c-runtime堆。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆,并且这个地址空间最初并没有对应的物理存储空间。除值类型外,CLR要求所有资源都从托管堆分配。
托管堆还维护着一个指针,我把它称为NextObjPtr。它指向下一个对象在堆中的分配位置。
IL指令newobj用于创建一个对象。C#提供了new操作符,它导致编译器在方法IL代码中生成一个newobj指令。newobj指令将导致CLR执行以下步骤(如何为类型分配内存?):
下图展示了3个对象(A,B和C)的一个托管堆。如果要分配新对象,它将放在NextObjPtr指针指向的位置(紧接着对象C后)。
应用程序调用new操作符创建对象时,可能没有足够的地址空间来分配该对象。托管堆将对象需要的字节数加到NextObjPtr指针中的地址上来检测这个情况。如果结果值超过了地址空间的末尾,表明托管堆已满,必须执行一次垃圾回收。
垃圾回收器检查托管堆中是否有应用程序不再使用的对象。如果有,它们使用的内存就可以被回收。那么,垃圾回收器是怎么知道一个对象不再被使用呢?
CPU寄存器(CPU Register)是CPU自己的“临时存储器”,比内存的存取还快。按与CPU远近来分,离得最近的是寄存器,然后缓存 (计算机一、二、三级缓存),最后内存。
每个应用程序都包含一组根(Roots)。每个根都是一个存储位置,他们可能指向托管堆上的某个地址,也可能是null。
类中定义的任何静态字段,方法的参数,局部变量(仅限引用类型变量)等都是根,另外cpu寄存器中的对象指针也是根。
例如,所有的全局和静态对象指针是应用程序的根,另外在线程栈上的局部变量/参数也是应用程序的根。只有引用类型的变量才被认为是根,值类型的变量永远不被认为是跟。
如果一个根引用了堆中的一个对象,则该对象为“可达”,否则即是“不可达”。被根引用的堆中的对象不被视为垃圾。
当垃圾回收器开始运行,它会假设托管堆上的所有对象都是垃圾。换句话说,它假设线程栈中没有引用堆中对象的变量,没有CPU寄存器引用堆中的对象,也没有静态字段引用堆中的对象。
垃圾回收分为2个阶段:
垃圾回收器的第一阶段是所谓的标记(marking)阶段。
垃圾回收器沿着线程栈上行以检查所有的根。如果发现一个根引用了一个对象,就在对象的“同步块索引字段”上开启一位(将这个bit设为1)---对象就是这样被标记的。当所有的根都检查完毕后,堆中将包含可达(已标记)与不可达(未标记)对象。
如下图,展示了一个堆,其中包含几个已分配的对象。应用程序的根直接引用对象ACDF,所有这些对象都被标记。标记好根和它的字段引用对象之后,垃圾回收器检查下一个根,并继续标记对象。如果垃圾回收器试图标记之前已经被标记过的对象,就会换一个路径继续遍历。这样做有两个目的:首先,垃圾回收器不会多次遍历一组对象,提高性能。其次,如果存在对象的循环链表,可以避免无限循环。
垃圾回收器的第二个阶段是压缩(compact)阶段。
在这个阶段中,垃圾回收器线性地遍历堆,以寻找未标记的连续内存块。如果发现大的可用的连续内存块,垃圾回收器会把非垃圾(标记/可达)的对象移动到这里来进行压缩堆。堆内存压缩后,托管堆的NextObjPtr指针将指向紧接在最后一个非垃圾对象之后的位置。这时候new操作符就可以继续成功的创建对象了。这个过程有点类似于磁盘空间的碎片整理。以此,对堆进行压缩,不会造成进程虚拟地址空间的碎片化。
如上图所示,绿色框表示可达对象,黄色框为不可达对象。不可达对象清除后,移动可达对象实现内存压缩(变得更紧凑)。
压缩之后,“指向这些对象的指针”的变量和CPU寄存器现在都会失效,垃圾回收器必须重新访问所有根,并修改它们来指向对象的新内存位置。这会造成显著的性能损失。这个损失也是托管堆的主要缺点。
基于以上特点,垃圾回收引发的回收算法也是一项研究课题。因为如果真等到托管堆满才开始执行垃圾回收,那就真的太“慢”了。
垃圾回收器的好处:
垃圾回收算法 --- 分代(Generation)算法
代是CLR垃圾回收器采用的一种机制,它唯一的目的就是提升应用程序的性能。分代回收,速度显然快于回收整个堆。
CLR托管堆支持3代:第0代,第1代,第2代。第0代的空间约为256KB,第1代约为2M,第2代约为10M。新构造的对象会被分配到第0代。
如上图所示,当第0代的空间满时,垃圾回收器启动回收,不可达对象(上图C、E)会被回收,存活的对象被归为第1代。
当第0代空间已满,第1代也开始有很多不可达对象以至空间将满时,这时两代垃圾都将被回收。存活下来的对象(可达对象),第0代升为第1代,第1代升为第2代。
实际CLR的代回收机制更加“智能”,如果新创建的对象生存周期很短,第0代垃圾也会立刻被垃圾回收器回收(不用等空间分配满)。另外,如果回收了第0代,发现还有很多对象“可达”,
并没有释放多少内存,就会增大第0代的预算至512KB,回收效果就会转变为:垃圾回收的次数将减少,但每次都会回收大量的内存。如果还没有释放多少内存,垃圾回收器将执行完全回收(3代),如果还是不够,则会抛出“内存溢出”异常。
也就是说,垃圾回收器会根据回收内存的大小,动态的调整每一代的分配空间预算!达到自动优化!
.NET垃圾回收器的基本工作原理是:通过最基本的标记清除原理,清除不可达对象;再像磁盘碎片整理一样压缩、整理可用内存;最后通过分代算法实现性能最优化。
终结(Finalization)是CLR提供的一种机制,允许对象在垃圾回收器回收其内存之前执行一些得体的清理工作。任何包装了本地资源(例如文件)的类型都必须支持终结操作。简单的说,类型实现了一个命名为Finalize的方法。当垃圾回收器判断一个对象是垃圾时,会调用对象的Finalize方法。
C#团队认为,Finalize方法是编程语言中需要特殊语法的一种方法。在C#中,必须在类名前加一个~符号来定义Finalize方法。
internal sealed class SomeType { ~SomeType() { //这里的代码会进入Finalize方法 } }
编译上述代码,会发现C#编译器实际是在模块的元数据中生成一个名为Finalize的protected override方法。方法主体被放到try块中,finally块放入了一个对base.Finalize的调用。
实现Finalize方法时,一般都会调用Win32 CloseHandle函数,并向该函数传递本地资源的句柄。
如果包装了本地资源的类型没有定义Finalize方法,本地资源就得不到关闭,导致资源泄露,直至进程终止。进程终止时,这些本地资源才会被操作系统回收。
不要对托管资源进行终结操作,终结操作几乎专供释放本地资源。
Finalize方法在垃圾回收结束时调用,有以下5种事件会导致开始垃圾回收:
终结操作表面看起来简单:创建一个对象,当它被回收时,它的Finalize方法会得到调用。但深究下去,远没有这么简单。
应用程序创建一个新对象时,new操作符会从堆中分配内存。如果对象的类型定义了Finalize方法,那么在该类型的实例构造器调用之前,会将一个指向该对象的指针放到一个终结列表 (finalization list) 中。终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象,在回收该对象之前,会先调用对象的Finalize方法。
下图展示了包含几个对象的一个托管堆。有的对象从应用程序的根可达,有的不可达(垃圾)。对象C,E,F,I,J被创建时,系统检测到这些对象的类型定义来了Finalize方法,所有指向这些对象的指针要添加到终结列表中。
垃圾回收开始时,对象B,E,G,H,I和J被判定为垃圾。垃圾回收器扫描终结列表以查找指向这些对象的指针。找到一个指针后,该指针会从终结列 表中移除,并追加到freachable队列中。freachable队列(发音是“F-reachable”)是垃圾回收器的内部数据结构。 Freachable队列中的每个指针都代表其Finalize方法已准备好调用的一个对象。
下图展示了回收完毕后托管堆的情况。从图中我们可以看出B,E和H已经从托管堆中回收了,因为它们没有Finalize方法,而E,I,J则暂时没有被回收,因为它们的Finalize方法还未调用。
一个特殊的高优先级的CLR线程负责调用Finalize方法。使用专用的线程可避免潜在的线程同步问题。freachable队列为空时,该线程将睡眠。当队列中有记录项时,该线程就会被唤醒,将每一项从freachable队列中移除,并调用每一项的 Finalize方法。
如果一个对象在freachable队列中,那么意味这该对象是可达的,不是垃圾。
原本,当对象不可达时,垃圾回收器将把该对象当成垃圾回收了,可是当对象进入freachable队列时,有奇迹般的”复活”了。然后,垃圾回收 器压缩(内存脆片整理)可回收的内存,特殊的CLR线程将清空freachable队列,并调用其中每个对象的Finalize方法。
垃圾回收器下一次回收时,发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachhable队列也不再指向它。所以,这些对象的内存会直接回收。
整个过程中,可终结对象需要执行两次垃圾回收器才能释放它们占用的内存。可在实际开发中,由于对象可能被提升到较老的一代,所以可能要求不止两次进行垃圾回收。下图展示了第二次垃圾回收后托管堆中的情况。
标签:
原文地址:http://www.cnblogs.com/chrisghb8812/p/5572591.html