zoukankan      html  css  js  c++  java
  • CLR的垃圾回收机制

    CLR的垃圾回收机制 (一)

    从这节开始就涉及CLR 最有意思的地方了,也是CLR 思想的核心部分,比较难理解,要反复思考才能有收获。这节是我对CLR的垃圾回收的整理,应用程序是如何构造新对象,托管堆如何控制这些对象的生存期,以及回收这些对象的内存。本节的内容主要还是参考CLR via C# 这本书,还有就是蒋金楠的博文。

     

    我们先大致了解一下CLR 中的资源概念、以及资源的生存周期

    CLR的垃圾回收机制用于清理废弃的资源,这些资源例如文件、网络连接、socket、数据库连接、内存等。在面向对象的环境中,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存,以下是访问一个资源所需要的步骤。

    • 1 调用IL 指令 newobj ,为代表资源的类型分配内存(一般使用C# new 操作符来完成)。

    • 2 初始化内存,设置资源的初始状态并使资源可使用。类型的实例构造器负责设置初始化状态。

    • 3 访问类型的成员来使用资源。

    • 4 摧毁资源的状态以进行清理。

    • 5 释放内存。垃圾回收器独自负责这一步。

    那应用程序是如何创建对象的呢?我们一步一步来解释这个问题 。C# 的new 操作符导致CLR 执行以下步骤。

    • 1 计算类型的字段所需要的字节数。

    • 2 加上对象的开销所需要的字节数。 每个对象都有两个开销字段:类型对象指针和同步块索引。32位应用程序,每个对象需要8 字节。64位应用程序每个对象需要 16字节。

    • 3 CLR 检查区域中是否有分配对象做需要的字节数,如果有足够的空间就在Newobj 指向的地址放入对象,为对象分配的字节会被清零。 调用类型构造器,new 操作符返回对象引用。在返回引用之前 NewObjPtr 指针的值会加上对象占用的字节数来得到一个新值,即下个对象放入托管堆时的地址。

    在许多应用中,差不多同时分配的对象彼此间有较强的联系,而其经常差不多在同一时刻访问。由于托管堆在内存中连续分配这些对象,所以会因为引用的“局部化”而获得性能上的提升。还意味着代码使用的对象可以全部驻留在CPU的缓存中。

     

    垃圾回收算法

    对于生存期的管理采用引用计数算法的弊端是循环引用。CLR采用的是 引用跟踪算法。 引用跟踪算法 只关心引用类型的变量,因为只有这种变量才能引用堆上的对象(值 类型变量直接包含值类型实例)。 我们将所有引用类型的变量都称为 (这里的变量是分配在栈中的,它引用了堆中的对象)。

    CLR 的GC 过程

    • 1 首先 暂停进程中的所有线程。

    • 2 然后进入GC 的标记阶段。CLR 遍历堆中所有对象,将他们的同步索引块中的某一位设为0 。

    • 3 然后CLR 检查所有活动的根,查看它们引用了哪些对象,那些根包含null 的被CLR忽略掉,继续查下一个根。 任何根如果引用了堆上的对象,CLR都会标记哪个对象,将它的同步索引块中的一位设为1 。 那些未被标记为1 的对象就要被垃圾回收了。

    • 4 进入GC 的压缩阶段 ,CLR 对已标记的对象进行“乾坤大挪移”,压缩所有幸存下来的对象(这里的压缩更近于 碎片整理),让它们占用连续的空间。同时,可用空间也全部是连续的。

    • 5 CLR 要保证每个根还是引用和之前一样的对象,只是对象在内存中变换了位置。

    • 6 托管堆的 NewObjPtr 指针指向最后一个幸存对象之后的位置。压缩阶段完成后,CLR 恢复应用程序的所有线程。

      如果进程的内存已经耗尽,此时试图分配更多内存的 new 操作会抛出 OutofMemoryException 。

    注意 !!!:静态字段会一直伴随进程存在,直到用于加载类型的 AppDomain 卸载为止。 内存泄漏的一个常见原因是让 静态字段引用某个集合对象。然后不停的向集合添加数据项。静态字段使集合一直存活,而集合对象使用所有数据项一直存活。

     

    代:提升性能

    CLR 的GC 是基于代的垃圾回收器 (generational garbage collector),它对你的代码做出了以下几点假设。

    • 1 对象越新,生存期越短。

    • 2 对象越老,生存期越长。

    • 3 回收堆的一部分,速度快于回收整个堆。

    在实际中第一代占用的内存远少于预算,所以垃圾回收器只检查第0 代中的对象。如果根或对象 引用了老一代的某个对象,垃圾回收器就可以忽略老对象的所有引用。

    如果根或对象也可能引用新对象。为确保对老对象的已更新字段进行检查,垃圾回收器利用了JIT 编译器内部的机制。这个机制在对象的引用字段发生变化时,会设置一个对应的标志位。

    托管堆只支持三代:第 0代,第 1代,第 2代。CLR初始化时,会为每一代选择预算。垃圾回收会动态的调整每一代的预算。垃圾回收器会根据应用程序要求的内存负载来自动优化。

     

    垃圾回收触发条件

    第一次垃圾回收后,第0代幸存下来的提升至第1代,垃圾回收1代后的幸存者会被提升至2代。什么时候回收1代呢?是1代对象占有的内存空间超过了预设的值的时候。什么时候回收0代呢?是0代对象占有内存空间超过了预设的值的时候。

    • 1 最常见的触发条件,CLR在检测第0 代超过预算时触发一次GC。

    • 2 代码显示调用 System.GC 的静态 Collect 方法。

    • 3 Windows 报告低内存情况。 CLR 内部使用win32函数 CreateMemoryResourceNotification 和 QueryMemoryResourceNotification 监视系统的总体内存使用情况。

    • 4 CLR 正在卸载AppDomain,进程停止的时候

    • 5 CLR正在关闭。

     

    大对象 目前认为 85000 字节或更大的对象是 大对象,CLR以不同的方式对待大小对象。

    • 1 大对象不是在小对象的地址空间分配,而是在进程地址空间的其他地方分配。

    • 2 目前版本的GC 不压缩大对象,因为在内存中移动它们代价过高。

    • 3 大对象总是第二代,所以只能为需要长时间存活的资源创建大对象。

     

    垃圾回收模式 CLR 启动时会选择一个GC 模式,进程终止前该模式不会改变。CLR 有两个基本GC 模式。

    • 1 工作站 该模式针对客户端应用程序优化GC , GC 造成的延时很低,应用程序线程挂起时间很短。该模式假定机器上运行的其他应用程序都不会太多消耗CPU的资源。

    • 2 服务器 该模式针对服务器端应用程序优化GC。被优化的主要是吞吐量和资源利用。该模式造成托管堆被拆分成几个区域,每个CPU一个。并发回收每个CPU自己的区域。

    除了这两种主要模式,GC还支持两种子模式:并发模式和非并发模式。

     

    监视应用程序的内存使用

    System.GC 类提供静态方法查看某一代发生了多少次垃圾回收,或者托管堆中的对象占用了多少内存。

    Int32 CollectionCount(Int32 generation);
    Int64 GetTotalMemory(Boolean forceFullCollection);

     

    *使用需要特殊清理的类型

    在编写应用程序中肯定会涉及例如:操作文件 FileStream、网络资源socket、互斥锁 Mutex 等这些本机资源。

    创建对象时不仅也要为它分配内存资源,还要为它分别本机资源。那么包含本机资源的类型被GC 时,GC会回收对象在托管堆中使用的内存,但这个类型的本机资源不清理的话,就会造成本机资源的泄漏。

    所以,CLR提供了称为 终结的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。任何包装了本机资源的类型都支持终结。

    CLR 判定一个对象不可达是,对象将终结它自己,释放它包装的本机资源。之后,GC 会从托管堆回收对象。

     

    对于使用了本机资源的对象,在废弃它的时候我们该如何处理呢?

    终极基类 System.Object 定义了受保护的虚方法 Finalize。如果你创建的对象使用了 本机资源,你可以要重写Object 的虚方法。在类名前添加~ 符号来定义Finalize方法。垃圾回收器判定对象是垃圾后,会调用对象的Finalize 方法。

    internal sealed class SomeType{
    ~SomeType() //这是一个Finalize方法
    {
    //这里的代码会进入Finalize 方法
    }
    }

    注意: 拥有本机资源的对象经历垃圾回收的顺序是这样的:

    • 1 拥有本机资源对象被标记为垃圾,等待GC 被清理。

    • 2 GC 将堆中其他垃圾回收完毕后才调用 Finalize方法,这些使用了本机资源的对象的内存没有被GC马上被回收,因为Finalize 方法可能要执行访问字段的代码。

    • 3 上一步导致拥有本机资源的对象被提升到下一代,使对象活得比正常时间长。

    • 4 当下一代对象被GC 回收时,拥有本机资源的对象的内存才会被回收。如果拥有本机资源的对象的字段引用了其他对象,那么它们也会被提升到下一代。

    使用Finlize 会造成到很多问题,普通的编程人员一旦用不好会造成很多麻烦。不过还好,在实际使用中,我们不会重写 Object 的Finalize 方法,而是使用FCL 中提供的辅助类。这些辅助类重写了 Finalize方法并添加了一些CLR “魔法”,这些魔法保证处理本机资源不会出错。使用者可以从这些辅助类派生出自己的类,从而继承CLR 的魔法。

    这些辅助类例如:System.Runtime.InteropServices.SafeHandle 抽象类、它派生了 SafeHandleZeroOrMinusOneIsInvalid 抽象类、它又派生了 SafeFileHandle、SafeRegistryHandle,SafeWaitHandle 和 SafeMemoryViewHandle 。

     

    包含本机资源的类一般不会让编程人员自己写的,我们只要懂得如何使用它们就好了。以常用的System.IO.FileStream 类为例:

     FileStream 对象在构造时会调用 Win32 的CreateFile函数,函数的句柄保存在 SafeFileHandle 对象中,然后通过FielStream 对象的一个私有字段来维护该对象的引用。

     

    其实对于释放本机资源的问题初学者不必理会,CLR已经做的很好了。如果想对可终结对象,也就是拥有本机资源的对象,它的内存如何释放探其原理的话需要好好看看CLR via C# 的垃圾回收这章。如果想知道 Finalize 的内部工作原理的话,我以后会另写一篇。

     

  • 相关阅读:
    c++中的extern
    DOS性能监视器
    谈谈.NET中的内存管理(转帖)
    static_cast和dynamic_cast
    关于对EventHandler 和e的理解(转帖)
    使用Windows Mobile 6模拟器上网的步骤(转帖)
    接口抽象类类
    当前不会命中断点 尚未加载指定的模块 windows mobile
    C# 编码的双重检验锁定
    Loadrunner 监控Unix系统性能指标的解释
  • 原文地址:https://www.cnblogs.com/mingjie-c/p/11695498.html
Copyright © 2011-2022 走看看