zoukankan      html  css  js  c++  java
  • 《CLR via C#》笔记——垃圾回收

    本篇目录

    1,垃圾回收的基本概念

        1.1 小对象堆和大对象堆 

        1.2 垃圾回收中的“代”

        1.3 堆和“代”的关系

    2,啥时执行垃圾回收?

    3,垃圾回收器是如何工作的?

        3.1标记无效对象。

        3.2 压缩阶段。

        3.3 终结揭秘

    4 ,Dispose模式:强制对象清理资源

    1,垃圾回收的基本概念

    1.1 小对象堆和大对象堆 

    我们都知道,CLR将我们的引用类型分配到托管堆上。这里指的托管堆实际是一个笼统的称呼。它实际是由一个小对象堆(small object heap,SOH)和一个大对象堆(large object heap,LOH)组成的。为对象分配空间时,将对象分为小对象(small object)和大对象(large object)。在NET Framework 1.1 和2.0中,超过85,000 bytes的对象就被称为大对象。很显然,LOH用于存储我们的大对象。对于85,000bytes这个数值,并不那么绝对。CLR内部可能会调节这个值,小于这个值的大对象也会分配到LOH中。堆中包含一个NextObjPtr指针,它总是指向堆末端最后一个有效对象之后的位置。

    1.2 垃圾回收中的“代”

    .Net垃圾回收器采用的是基于代(generagion)的垃圾回收机制。它做出了如下的假设:对象越新,生存期越短;对象越老,生存期越长;回收堆的一部分,速度快于整个堆。共分为3代:0代、1代、2代。最新分配的小对象,都属于0代。垃圾回收执行一次后,仍然存活的对象被划为1代;执行第二次后还能存活的,被划入2代;执行三次及以上还能存活的,还是属于2代(这是最顶级了,不能再高了)。

    CLR初始化时,会为每一代初始化一个阀值,例如:0代(256kb),1代(2M),2代(16M)。当每一代的对象超过这个阀值时,就会执行一次垃圾回收。另外要注意,执行1代的垃圾回收会包括0代,执行2代的垃圾回收会包括1代和0代。执行2代的垃圾回收相当于是对整个托管堆的垃圾回收。

    代数越小,它的阀值也越小。由于每次超过阀值就会自动执行垃圾回收,所以它的垃圾回收频率自然也最高。

    1.3 堆和“代”的关系

      小对象堆上创建的对象都属于0代。大对象堆上创建的对象都属于2代。我们不能创建一个1代的对象。只有垃圾回收器回收一次后仍然存活的对象才能成为1代。所以,小对象堆上可能同时存在0,1,2代的对象。

    2,啥时执行垃圾回收?

    1. 0代满时 ,这是最常见的一种方式。因为随着应用程序代码运行并分配新对象,这个事件会自然而然的发生。
    2. 代码显示调用System.GC的静态方法。代码显示的请求CLR执行垃圾回收。
    3. Window报告内存不足。CLR强制执行垃圾回收,尝试释放已经死亡的对象,从而减少应用程序的占用的内存。
    4. CLR卸载AppDomain。一个AppDomain被卸载,CLR认为该AppDomain中不再存在任何根,因此会对所有代的对象执行垃圾回收。
    5. CLR关闭。一个进程正常终止时(不是从任务管理器强制关掉),CLR认为进程中不存在任何根,因此会调用所有对象的Finalize方法。但不会执行压缩和释放内存,进程终止后,由Windows来回收内存。

    3,垃圾回收器是如何工作的?

        垃圾回收主要由标记(marking)无效对象,压缩(compact),执行终结器(Finalize)队列,回收内存等几个主要步骤。

        首先,垃圾回收器必须要知道一个应用程序是否在使用一个对象,那它是怎么知道的呢?这里牵涉到一个概念:(root)。应用程序都包含一组根,每个根是一个存储位置,其中包含指向引用类型对象的一个指针。这个指针,要么指向托管堆的一个对象,要么为null。包含根的位置只有:静态字段、方法参数、局部变量、CPU寄存器

    3.1标记无效对象。

    垃圾回收开始时,会假设堆中所有的对象都是垃圾。然后逐一检查所有根。如果一个根引用了一个对象,就会在该对象的“同步块索引字段”上开启一位标志——对象就是这样被标记的。如果根引用的对象又引用了其他的对象,垃圾回收器会沿着这条路进行递归的标记。没有被标记的对象就成了无效对象。为了避免出现无限循环,对已经标记的对象,就不会沿着这条路走下去。

    3.2 压缩阶段。

    垃圾回收器线性遍历堆,寻找未标记对象的连续内存块。如果该内存块较小,垃圾回收器会忽略它;如果内存块较大,垃圾回收器就会把非垃圾对象移动到这里以压缩堆。这样就可以使得堆的末端始终腾出足够的空间以分配新的对象。这里有一个例外,大对象堆中进行垃圾回收时不会进行压缩处理,这是由于大对象的移动代价过于昂贵。压缩阶段还会更新一些对象的引用,因为新对象被移动到了新的位置,参照这些对象的引用都需要更新为新的位置。

    3.3 终结揭秘

    终结(finalization)是CLR提供的一种机制,允许对象在被垃圾回收器回收其内存之前执行一些必要的清理操作。通常是清理一些非托管的资源(如文件,网络连接,互斥体等)。一个类型只要实现了一个叫Finalize的方法,垃圾回收器在回收对象时就会调用这个方法,以保证这个对象在“临死之前还能吃上最后一顿”。对于C#来说,这个方法的定义是在类名前加一个~符号。

        public class SomeType
        {
            ~SomeType(){ 
            //这里的代码会进入Finalize方法
            }
        }

    从表面上看,终结操作似乎很简单:创建一个对象,当它被回收时,调用它的Finalize方法。但一旦深入研究,就会发现它远非那么简单。应用程序在创建对象时,如果发现它的类型定义了Finalize方法,就会在它的构造器调用之前,将该对象的一个指针放到一个终结列表(finalizaton list)中。终结列表是垃圾回收器控制的一个数据结构。如果一个对象被判定为垃圾,垃圾回收器会扫描终结列表,查找指向垃圾对象的指针。如果找到该指针,就会把它移除,并追加到一个freachable队列中。freachable队列也是垃圾回收器的一个内部数据结构,CLR启动一个特殊的高优先级的线程来专门执行freachable队列中对象的Finalize方法。

        当一个对象不可达时,垃圾回收器将其视为垃圾。但是,当垃圾回收器将对象的引用从终结列表移至freachable队列时,对象不再被视为垃圾,其内存不能被回收。标记freachable对象时,这些对象的引用类型字段引用的对象也会被递归的标记,所有这些对象又从垃圾回收的过程中“复活”了。然后,垃圾回收器开始压缩内存,特殊的线程清空freachable队列,并执行每个对象的Finalize方法。垃圾回收器下一次调用时,会发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachable队列也不再指向它。所以,这些对象的内存会被直接回收。在整个过程中,注意可终结的对象执行两次垃圾回收才能释放它们的内存,由于对象可能被提升至另一代,所以可能还不止两次垃圾回收。

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

        Finalize方法非常有用,它保证了当托管对象被释放时,本地资源不会泄漏。但Finalize的问题在于,它的调用时间是没有保证的,也就是不确定的。并且它不是一个公共的方法,不能显示的调用它。

        类型如果需要显示的、确定的清理资源,需要实现Dispose模式。Dispose模式较为复杂,大致的实现方式如下:

        public class SomeType : IDisposable
        {
            //这是一个标记,表明对象已经释放
            private bool _diposed = false;
    
            //这个方法是可选的,它的功能和Dispose方法一样
            //主要是为了那些不熟悉Dispose模式的用户调用
            public void Close()
            {
                Dispose(true);
            }
            //该方法实现IDispose的Dispose
            //调用这个公共方法来确定性地关闭资源
            public void Dispose()
            {
                Dispose(true);
            }
            ~SomeType()
            {
                //调用实际执行清理的方法
                Dispose(false);
            }
    
            //这个一个执行清理的常用方法
            //Finalize,Dispose,Close都要调用这个方法
            //如果这个类是密封的(sealed),protected virtual 应改为private
            protected virtual void Dispose(Boolean disposing)
            {
                //如果已经释放,直接返回
                if (!_diposed)
                {
                    if (disposing)
                    {
                       //在这个if语句中,对象正在被显示的dispose,可以访问对象的字段
                       //因为Finalize方法还没有执行,所以在这里可以释放托管资源
                    }
                    //在这里释放本地资源(如文件,套接字等)
                    _diposed = true;
                    //阻止调用Finalize方法
                    GC.SuppressFinalize(this);
                }
            }
        }

     有几点需要说明,任何实现了IDispose接口的类型,就相当于声称自己遵循Dispose模式。无参Dispose方法和Close方法应该可以被多次调用而不抛出异常,第二次调用只是简单的返回。对于GC.SuppressFinalize(this);这句代码是为了阻止Finalize方法被再次调用。如果是从Dispose方法和Close方法显示调用来释放对象,确实不应该再调用Finalize方法。但如果没有从这两个方法显示释放对象,对象就会从Finalize方法来释放对象,可能有人会觉得在Finalize方法中再次调用GC.SuppressFinalize(this);会出问题。但这样不会有任何问题,因为对象已经处于被终结过程中。调用GC.SuppressFinalize(this)方法,会设定与this关联的对象的一个标志位,有了这个标志位,CLR就知道从终结列表移除该对象的指针时,不应该将该对象的指针添加到freachable队列,从而阻止调用对象的Finalize方法。

     

  • 相关阅读:
    linux 命令——48 watch (转)
    linux 命令——47 iostat (转)
    linux 命令——46 vmstat(转)
    linux 命令——45 free(转)
    linux 命令——44 top (转)
    linux 命令——43 killall(转)
    linux 命令——42 kill (转)
    linux 命令——41 ps(转)
    linux 命令——40 wc (转)
    Java for LeetCode 068 Text Justification
  • 原文地址:https://www.cnblogs.com/xiashengwang/p/2617373.html
Copyright © 2011-2022 走看看