• CLR Via CSharp读书笔记(21):自动内存管理(垃圾回收)


    #1 垃圾回收平台的基本工作原理

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

    1)调用IL指令newobj,为代表资源的类型分配内存。在C#中使用new操作符,编译器就会自动生成该指令。
    2)初始化内存,设置资源的初始状态,使资源可用。类型的实例构造器负责设置该初始状态。
    3)访问类型的成员(可根据需要反复)来使用资源。
    4)摧毁资源的状态以进行清理。正确清理资源的代码要放在Finalize, DisposeClose方法。
    5)释放内存。垃圾回收器独自负责这一步。

    托管堆如何知道应用程序不再用一个对象

    托管堆是CLR中自动内存管理的基础。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针(NextObjPtr),用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。

    newobj指令将导致CLR执行以下步骤:

    1) 计算类型(及其所有基类型)的字段所需要的字节数

    2) 加上对象的开销所需要的字节数。包括类型对象指针同步索引块。32位程序需要增加8字节,64位程序需要增加16字节。

    3) CLR检查保留区域是否能够提供分配对象所需要的字节数,如有必要就提交存储。如果托管堆有足够的可用空间,对象会被放入。

    托管堆上连续分配的对象会由于引用的locality而获得性能上的提升,而且对象可以全部驻留在CPU缓存中,不会因为cache miss而被迫访问较慢的RAM.

    托管堆之所有有这些好处,是因为它做了一个假设--地址空间和存储是无限的。托管堆通过垃圾回收器来允许它做这样的假设。

    应用程序调用new操作符创建对象时,如果第0代堆满,执行一次垃圾回收。在一次垃圾回收中存活下来的对象被提升到另一代(例如第1代)。

    #2 垃圾回收算法

    每个应用程序都包含一组根(root)。静态字段方法参数局部变量均被认为是一个根。只有引用类型的变量才被认为是根。值类型的变量永远都不被认为是根。此外,CPU寄存器也被视作根。

    垃圾回收器开始执行时,假设堆中的所有对象都是垃圾,然后通过标记(对有跟引用的进行标记)和压缩(回收没有标记的对象)进行垃圾回收。

    标记阶段:垃圾回收器沿着线程栈上行以检查所有根,如果发现一个跟(root)引用了一个对象(直接引用或间接引用),就在对象的"同步索引字段"上开启一位(将一个bit设置为1)进行标记。

    压缩阶段:垃圾回收器线性的遍历堆,以寻找未标记对象的连续内存块。若果内存块较小,垃圾回收器会忽略该块。移动内存中的对象后,包含"指向这些对象的指针"的变量和CPU寄存器现在都会变得无效,垃圾回收器需要遍历修改所有根来指向新的内存位置。

    #3 垃圾回收与调试

    class DebuggingRoots {
        public static void Go() {
            var t = new System.Threading.Timer(TimerCallback, null, 0, 2000);
            Console.ReadLine();
    
            // 在ReadLine之后引用t,这种方式会被编译器优化掉
            //t = null;
    
            // 在ReadLine之后引用t,防止其在Dispose方法返回之前被垃圾回收
            //t.Dispose();
        }
    
        private static void TimerCallback(Object o) {
            Console.WriteLine("In TimerCallback: " + DateTime.Now);
            // 出于演示目的强制执行垃圾回收
            GC.Collect();
        }
    }

    #4 使用终结操作释放本地资源

    System.Threading.Mutex类型要打开一个Windows互斥体内核对象(本地资源)并保存其句柄,并在调用Mutex的方法时使用该句柄。

    值类型(含所有枚举类型)、集合类型、String、Attribute、Delegate和Exception所代表的资源无需执行特殊的清理操作。如果一个类型代表着(或包装着)一个非托管资源(比如文件、数据库连接、套接字、mutex、位图、图标等),在对象的内存准备回收时,必须执行一些清理代码。实现了Finalize方法的任何类型实际上是在说,它的所有对象都希望"在被处决之前吃上最后一餐"。

    1) 使用CriticalFinalizerObject类型确保终结

    CLR赋予这个类以下三个功能:

    首次构造任何CriticalFinalizerObject派生类型的一个对象时,CLR立即对继承层次结构中的所有Finalize方法进行JIT编译以确保其肯定得到执行。如果不对Finalize方法进行提前编译,则在内存紧张时,CLR可能找不到足够的内存来编译Finalize方法,这会阻止方法的执行,造成本地资源泄露。此外,如果Finalize方法中的代码引用了另外一个程序集中的一个类型,而且CLR在寻找这个程序集时失败,也会造成资源得不到释放。

    CLR是在调用了非CriticalFinalizerObject派生类型的Finalize方法之后,才调用CriticalFinalizerObject派生类型的Finalize方法。例如,FileStream类的Finalize方法可以放心的将数据从内存缓冲区flush到磁盘,它知道此时磁盘文件还没有关闭

    如果AppDomain被一个宿主应用程序(SQL Server或Asp.NET)强行中断,CLR将调用CriticalFinalizerObject派生类型的Finalize方法。

    2) SafeHandle类型及其派生类型

    对于SafeHandle类,需要注意: 其一,它派生自CriticalFinalizerObject,这确保它会得到CLR的特殊对待; 其二,它是一个抽象类,必须有另外一个类从该类派生,并重写受保护的构造器、抽象方法ReleaseHandle以及抽象属性IsValidget访问器方法。

    3)使用SafeHandle类型与非托管代码进行互操作

    与非托管代码进行交互时,SafeHandle提供了另外两个功能: CLR调用Win32 CreateEvent函数,函数返回到托管代码时,新的SafeWaitHandle(CLR知道其从SafeHandle派生)对象的构造以及句柄的赋值是在非托管代码中发生的,不可能被一个ThreadAbortException打断。

    SafeHandle派生类的最后一个功能是防止有人利用潜在的安全漏洞。当一个线程可能试图使用一个本地资源,同时另一个线程试图释放该资源,这可能造成一个句柄循环使用漏洞。SafeHandle类防范这个安全隐患的办法是使用引用计数

    CriticalHandle类除了不提供引用计数器功能,其他方面与SafeHandle类相同。CriticalHandle类及其派生类通过牺牲安全性来换取更好的性能。

    #5 对托管资源使用终结操作:

    虽然终结操作几乎专供释放本地资源,但偶尔也用于托管资源。Finalize方法中调用的任何代码都不能使用其他任何可能已经终结的对象

    即使类型的实例构造器抛出了异常,类型的Finalize方法也会被调用。因此,Finalize方法不应假设对象处于良好、一致的状态。

    设计一个类型时,处于性能方面的原因,最好避免使用Finalize方法,因为必须进行额外的处理(分配的时候要将指针放到终结列表,对象可能会提升到较老的代回收时须进行额外处理).

    Finalize方法在垃圾回收发生时运行,而垃圾回收可能在应用程序请求更多内存时才发生;CLR不保证各个Finalize方法的调用顺序;调用静态方法需要注意方法是否在内部访问已终结了的对象。完全可以放心的访问值类型的实例,或者没有定义Finalize方法的引用类型的实例

    #6 导致Finalize方法被调用的5种事件

    第0代满(最常见的方式)
    代码显示调用System.GC的静态方法Collect
    Windows报告内存不足
    CLR卸载AppDomain
    CLR关闭

    每个Finalize方法大约有2秒时间返回,所有Finalize方法40秒钟进行返回。如果超时,CLR会直接杀死进程。

    #7 终结操作揭秘:

    终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象--该对象定义了Finalize方法,在回收该对象的内存之前,应调用它的Finalize方法。

    被判定为垃圾的对象,如果定义了Finalize方法,其终结列表中的指针将被移至freachable队列中。一个特殊的高优先级CLR线程负责调用Finalize方法。如果一个对象在freachable队列中,它就是可达的,不是垃圾。可终结的对象需要执行两次或以上(提升至另一代)垃圾回收才能释放他们占用的内存

    #8 Dispose模式:强制对象清理资源:

    public abstract class SafeHandleEx : IDisposable
    {
        public void Dispose()
        {
            // 传递true,导致可以安全的访问引用其他对象的字段,因为其他这些对象的Finalize方法还没有调用
            this.Dispose(true);
        }
    
        public void Close()
        {
            // 传递true,导致可以安全的访问引用其他对象的字段,因为其他这些对象的Finalize方法还没有调用
            this.Dispose(true);
        }
    
        public ~SafeHandleEx()
        {
            // 传递false,导致不能安全的访问引用其他对象的字段,因为其他这些对象的Finalize方法可能已经调用
            this.Dispose(false);
        }
    
        protected virtual void Dispose(Boolean disposing)
        {
            if (disposing)
            {
                // 在该if语句中,可以安全的访问引用其他对象的字段,因为其他这些对象的Finalize方法还没有调用
            }
    
            GC.SuppressFinalize(this);
        }
    }

    #9 使用实现了Dispose模式的类型

    public static void Go() {
        Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 };
        FileStream fs = new FileStream("Temp.dat", FileMode.Create);
        fs.Write(bytesToWrite, 0, bytesToWrite.Length);
    
        // 显示关闭文件
        ((IDisposable)fs).Dispose();  
        // 必须调用Dispose之后调用该方法,否则报告文件正在被占用
    File.Delete("Temp.dat");
    }

    确定必须清理资源时,确定可以安全的调用Dispose或Close,并希望将对象从终结表中删除,禁止对象提升从而提升性能时,才调用Dispose或Close。

    #10 使用C# using语句

    #11 StreamWriter对FileStream的依赖

    #12 手动监视和控制对象的生存期(GCHandle)

  • 相关阅读:
    Elasticsearch Server,2nd Edition pdf 翻译 中文
    为什么氦气会改变人的音色?
    intellij 文件太大,无法code assistant
    intellij idea rearrange code
    static变量、static方法之间的异同
    linux top 各个标识的含义 详解
    JAVA面向对象编程课程设计——多人扫雷对战
    递归法创建二叉树
    线性表总结
    线性表总结
  • 原文地址:https://www.cnblogs.com/thlzhf/p/2805491.html
Copyright © 2020-2023  润新知