zoukankan      html  css  js  c++  java
  • 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)

  • 相关阅读:
    Time Zone 【模拟时区转换】(HDU暑假2018多校第一场)
    HDU 1281 棋盘游戏 【二分图最大匹配】
    Codeforces Round #527 (Div. 3) F. Tree with Maximum Cost 【DFS换根 || 树形dp】
    Codeforces Round #527 (Div. 3) D2. Great Vova Wall (Version 2) 【思维】
    Codeforces Round #527 (Div. 3) D1. Great Vova Wall (Version 1) 【思维】
    Codeforces Round #528 (Div. 2, based on Technocup 2019 Elimination Round 4) C. Connect Three 【模拟】
    Avito Cool Challenge 2018 E. Missing Numbers 【枚举】
    Avito Cool Challenge 2018 C. Colorful Bricks 【排列组合】
    005 如何分析问题框架
    004 如何定义和澄清问题
  • 原文地址:https://www.cnblogs.com/thlzhf/p/2805491.html
Copyright © 2011-2022 走看看