码迷,mamicode.com
首页 > 其他好文 > 详细

第21章 自动内存管理 21.1-21.7

时间:2016-06-09 18:39:38      阅读:282      评论:0      收藏:0      [点我收藏+]

标签:

本章将讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存。简单的说,本章要解释CLR中的垃圾回收器是如何工作的,还要解释与它有关的性能问题。

21.1理解垃圾回收平台的基本工作原理

在.NET Framework中,内存中的资源(即所有二进制信息的集合)分为“托管资源”和“非托管资源”。托管资源必须接受.NET Framework的CLR的管理 (如内存类型安全性检查) 。而非托管资源则不必接受.NET Framework的CLR管理, 需要手动清理垃圾(显式释放)。注意,“垃圾回收”机制是.NET Framework的特性,而不是C#的。

每个程序都要使用这样或那样的资源,比如文件、内存缓冲区、屏幕空间、网络连接、数据库资源等。事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存。

以下是访问一个资源所需的具体步骤:

  • 在C#中使用new操作符创建一个新对象,编译器就会自动生成IL指令newobj,为代表此资源的类型分配内存。(如何分配见下面)
  • 初始化内存,设置资源的初始状态,使资源可用。类型的实例构造器负责设置该初始化状态。
  • 访问类型的成员来使用资源。
  • 摧毁资源的状态以进行清理。
  • 释放内存,垃圾回收器独自负责这一块。

垃圾回收(garbage collection)自动发现和回收不再使用的内存,不需要程序员的协助。使开发人员得到了解放,现在不必跟踪内存的使用,也不必知道在什么时候释放内存。但是,垃圾回收器不可以管理内存中的所有资源,对内存中的类型所代表的资源也是一无所知的。这意味着垃圾回收器不知道怎么执行“摧毁资源的状态以进行清理”。这部分资源就需要开发人员自己写代码实现回收。在.Net framework中,开发人员通常会把清理这类资源的代码写到Dispose,Finalize和Close方法中。

在.net中提供三种模式来回收内存资源:dispose模式,finalize方法,close方法:

  • dispose提供了一种显示释放内存资源的方法。Dispose调用方法是: 要释放的资源对象.dispose()
  • finalize方法是.net的内部的一个释放内存资源的方法。这个方法不对外公开,由垃圾回收器自动调用
  • close和dispose其实一样,只不过有的对象没有提供dispose的方法,只提供了close方法,而close其实在那个对象的类中,依然是调用了一个私有的dispose方法,而finalize其实也是调用一个不对外公开的dispose方法

然而,值类型、集合类型、String、Attribute、Delegate和Exception所代表的资源无需执行特殊的清理操作。列如,只需销毁对象的内存中维护的字符数组,一个String资源就会被完全清理。

值类型(包括引用和对象实例)和引用类型的引用其实是不需要什么“垃圾回收器”来释放内存的,因为当它们出了作用域后会自动释放所占内存(因为它们都保存在“堆栈”中,学过数据结构可知这是一种先进后出的结构)。只有引用类型的引用所指向的对象实例才保存在“堆”中,而堆因为是一个自由存储空间,所以它并没有像“堆栈”那样有生存期 (“堆栈”的元素弹出后就代 表生存期结束,也就代表释放了内存)。并且非常要注意的是,“垃圾回收器”只对“堆”这块区域起作用。

从托管堆分配资源

.Net clr把所有的引用对象都分配到托管堆上,这一点很像c-runtime堆。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆,并且这个地址空间最初并没有对应的物理存储空间。除值类型外,CLR要求所有资源都从托管堆分配。

托管堆还维护着一个指针,我把它称为NextObjPtr。它指向下一个对象在堆中的分配位置。

IL指令newobj用于创建一个对象。C#提供了new操作符,它导致编译器在方法IL代码中生成一个newobj指令。newobj指令将导致CLR执行以下步骤(如何为类型分配内存?):

  • 计算类型的字段需要的字节数。
  • 加上对象的开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引。
  • CLR检查保留区域是否能够提供分配对象所需的字节数,如有必要就提交存储。如果托管堆有足够的可用空间,对象会被放入。该对象是在NextObjPtr指针指向的地址放入的,并且为它分配的字节会被清零。接着,调用类型的实例构造器(为this参数传递NextObjPtr),IL指令newobj将返回对象的地址。就在地址返回之前,NextObjPtr指针的值会加上对象占据的字节数,这样会得到一个新值,它指向下一个对象放入托管堆时的地址。

下图展示了3个对象(A,B和C)的一个托管堆。如果要分配新对象,它将放在NextObjPtr指针指向的位置(紧接着对象C后)。

技术分享

应用程序调用new操作符创建对象时,可能没有足够的地址空间来分配该对象。托管堆将对象需要的字节数加到NextObjPtr指针中的地址上来检测这个情况。如果结果值超过了地址空间的末尾,表明托管堆已满,必须执行一次垃圾回收。

21.2 垃圾回收算法

垃圾回收器检查托管堆中是否有应用程序不再使用的对象。如果有,它们使用的内存就可以被回收。那么,垃圾回收器是怎么知道一个对象不再被使用呢?

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垃圾回收器的基本工作原理是:通过最基本的标记清除原理,清除不可达对象;再像磁盘碎片整理一样压缩、整理可用内存;最后通过分代算法实现性能最优化。

21.4使用终结操作来释放本地资源

终结(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方法,本地资源就得不到关闭,导致资源泄露,直至进程终止。进程终止时,这些本地资源才会被操作系统回收。

21.5对托管资源使用终结操作

不要对托管资源进行终结操作,终结操作几乎专供释放本地资源。

21.6 什么会导致Finalize方法被调用

Finalize方法在垃圾回收结束时调用,有以下5种事件会导致开始垃圾回收:

  • 第0代满 只有第0代满时,垃圾回收器会自动开始。该事件是目前导致调用Finalize方法最常见的一种方式。
  • 代码显式调用System.GC的静态方法Collect  代码可以显式请求CLR执行即时垃圾回收操作。
  • Windows内存不足 当Windows报告内存不足时,CLR会强制执行垃圾回收。
  • CLR卸载AppDomain 一个ApppDomain被卸载时,CLR认为该AppDomain中不存在任何根,因此会对所有代的对象执行垃圾回收。
  • CLR关闭 一个进程正常终止时(比如通过任务管理器关闭),CLR就会关闭。CLR关闭会认为进程中不存在 任何根,因此会调用托管堆中所有对象的Finalize方法。此时,CLR不会尝试压缩或释放内存,因为整个进程都要终止,由Windows回收进程中所有内存。

21.7终结操作揭秘

终结操作表面看起来简单:创建一个对象,当它被回收时,它的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队列也不再指向它。所以,这些对象的内存会直接回收。

 整个过程中,可终结对象需要执行两次垃圾回收器才能释放它们占用的内存。可在实际开发中,由于对象可能被提升到较老的一代,所以可能要求不止两次进行垃圾回收。下图展示了第二次垃圾回收后托管堆中的情况。

技术分享

第21章 自动内存管理 21.1-21.7

标签:

原文地址:http://www.cnblogs.com/chrisghb8812/p/5572591.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!