• 内存分析_.Net垃圾回收介绍


    垃圾回收

    1.       .Net垃圾回收中涉及的名称

    1.1.什么是代?

    垃圾回收器为了提升性能使用了代的机制,共分为三代(Gen0、Gen1、Gen2)。GC工作机制基于以下假设,

    1)  对象越新,生存期越短

    2)  对象越老,生存期越长

    3)  回收堆的一部分比回收整个堆时间短

    在应用程序的生命周期中,最近新建的对象被分配在第0代,在一次垃圾回收之后存活下来的进入下一代。这样可以使GC专注于回收最有可能存在更多可回收对象的第0代(最近分配的最有可能很快被释放)

    1.2 什么时候发生垃圾回收?

    1)  第0代满工作原理

    2)  代码显示调用GC.Collect方法

    3)  Windows报告内存不足

    CLR注册了Win32,CreateMemoryResourceNotification和QueryMemoryResourceNotification监视系统总体内存使用情况,如果收到window报告内存不足的通知,强行执行GC

    4)  CLR卸载AppDomain

    5)  CLR关闭

    1.3什么是大对象堆?

    采用大对象堆是垃圾回收另外一个性能提升的策略,任何大于等于85000bytes的对象都被视为大对象在特殊的大对象堆中分配。

    大对象堆的回收策略:

    (1)       大对象堆被认为是第2代的一部分,大对象堆回收时候同时回收第2代

    (2)       大对象堆不进行压缩操作(因为太耗时耗力)

    根据该策略我们可以推测如果大对象频繁的被分配将造成频繁的第2代垃圾回收(即完全垃圾回收),对性能造成较大影响。

    1.4什么是root?

    静态对象

    方法参数

    局部变量

    CPU寄存器

    1.5什么是finalizer?

    大多数时候我们创建的类不包含非托管资源,因此只需要直接使用,CLR自然会判断其生命周期结束而后回收相应的托管资源。但如果我们创建了含有非托管资源的类,CLR提供了finalizer机制来帮助自动释放非托管资源。

    实现finalizer的语法和析构函数相似,实现了这个类似于析构函数的方式实际上被隐式转换成了重载父类Finalize方法(object类默认提供了finalize方法)。

    Class car

    {

    ~car()//destructor

      {

          //cleanup statements

      }

    }

    转换后

    Protected override void Finalize()

    {

        Try

    {

        //cleanup statements….

    }

    Finally

    {

       Base.Finalize();

    }

    }

    1.6什么是finalizequeue?

     在新建一个类的实例时,如果该类定义了Finalize方法,那么该类在构造器调用之前会将指向该对象的指针存放在一个叫finalization list中。垃圾回收时如果该对象被认定为垃圾,那么CLR会从finalizationlist中查找是否存在相应的对象指针,如果存在则将该指针移除,然后在freachable队列中加入该对象指针,CLR提供了一个高优先级的Finalizer线程来专门负责调用freachable队列中对象的finalize方法以释放资源。

    1.7什么情况下会发生Out of memory exception?

    在一个内存分配请求到达时,CLR发现第0代没有足够空间从而触发第0代GC,如果还是没有足够的内存,CLR发起完全GC,接下来CLR尝试增大第0代的大小,如果没有足够的地址空间来增大第0代大小或满足内存分配请求,就会抛出OutOfMemoryException。

    因此发生OutOfMemoryException的两个可能性是:

    (1)       虚拟地址空间耗尽

    (2)       物理内存耗尽

    1.8什么情况下要实现IDisposible接口?

    IDisposible最重要的目的是释放非托管资源,垃圾回收可以自动回收托管资源,但是对于程序中使用的非托管资源却一无所知,例如数据库连接、对象句柄等

    MSDN中给了正确的IDisposable接口的正确实现,这个实现中最容易被误解的是protected virtual void Dispose(bool disposing)方法中布尔参数disposing的作用是什么。

    参数disposing的目的是在显示调用Dispose方法或隐式调用Finalizer的情况下区别对待托管资源,在两种情况下对于非托管资源的处理是一致的,直接释放,不应该将非托管资源的释放放在if(disposing)的处理中。

    为什么要区别对待托管资源?在显示调用dispose方法的时候可以保证其内部引用了托管资源未被回收,所有可以直接调用其相应的释放方法。但是finalizer被调用dispose的方法时,由于GC无法保证托管资源的释放顺序,在dispose方法中不应该再去访问内部的托管资源,有可能内部的托管资源已经被释放掉了。

    1.9什么情况下用GC.Collect?

    大多数情况下我们都应该避免调用GC.Collect方法,让垃圾回收器自动执行,但是还是有些情况比如在某个时刻会发生一次非重复性事件导致大量的对象死亡,这个时候我们可以不依赖于垃圾回收器的自动机制,手动调用GC.Collect方法。记住不要为了改善应用程序相应时间而调用GC.Collect,而是应该处于减少工作集的目的。

     通过编程使用GC.Collect()强制进行可能会有好处。说得更明确就是:

    (1)       应用程序将要进入一段代码,后者不希望被可能的垃圾回收中断。

    (2)       应用程序刚刚分配非常多的对象,你想尽可能多地删除已获得的内存。

    2.       托管堆优化

    .Net框架包含一个托管堆,所有的.Net语言在分配引用类型对象时都要使用它。像值类型这样的轻量级对象始终分配在栈中,但是所有的类实例和数组都被生成在一个内存池中,这个内存池就是托管堆

    垃圾收集器的基本算法很简单:

    (1)       将所有的托管内存标记为垃圾

    (2)       寻找正被使用的内存块,并将他们标记为有效

    (3)       释放所有没有被使用的内存块

    (4)       整理堆以减少碎片

    垃圾收集器遍历整个内存池开销很高,然而,大部分在托管堆上分配的对象只有很短的生存期,因此堆被分成3个段,新分配的对象被放在generation()中,这个generation是最先被回收的—在这个generation中最有可能找到不再使用的内存,由于它的尺寸很小(小到足以放进处理器的L2cache中),因此在它里面的回收将是最快和最有效的。

    托管堆的另外一种优化操作与locality ofreference规则有关,该规则表明,一起分配的对象经常被一起使用。如果对象们在堆中位置很紧凑的话,高速缓存的性能将会得到提高。由于托管堆的的天性,对象们总是被分配在连续的地址上,托管堆总是保持紧凑,结果使得对象们失踪彼此靠近,永远不会分的很远。这一点与标准堆提供的非托管代码形成了鲜明的对比,在标准堆中,堆很容易变成碎片,而且一起分配的对象经常分的很远。

    还有一种优化是与大对象有关的。通常,大对象具有很长的生存期,当一个大对象在.net托管堆中产生时,它被分配在堆的一个特殊部分中,这部分堆永远不会被整理。因为移动大对象所带来的开销超过了整理这部分堆所能提高的性能。

    3.       外部资源

    垃圾收集器能够有效地管理从托管堆中释放的资源,但是资源回收操作只有在内存紧张而触发一个回收动作时才执行。那么。类是怎样来管理像数据库连接或者窗口句柄这样有限的资源的呢?

    所有拥有外部资源的类,在这些资源已经不再用到的时候,都应当执行close或者Dispose方法,从.Net FrameworkBeta2开始,Dispose模式通过IDisposable接口来实现。

    需要清理外部资源的类还应当实现一个终止操作(finalizer)。在C#中,创建终止操作的首选方式是在析构函数中实现,而在Framework层,终止操作的实现则是通过重载System.Object.Finalize方法。以下两种实现终止操作的方法是等效的:

      ~OverdueBookLocator()

      {

       Dispose(false);

      }

      和:

      public void Finalize()

      {

       base.Finalize();

       Dispose(false);

      }

    在C#中,同时在Finalize方法和析构函数实现终止操作将会导致错误的产生。

    除非你有足够的理由,否则你不应该创建析构函数或者Finalize方法。终止操作会降低系统的性能,并且增加执行期的内存开销。同时,由于终止操作被执行的方式,你并不能保证何时一个终止操作会被执行。

    4.       Finalize()-终结和Dispose()-处置

    维护内部非托管资源的托管类的手段:Finalize()--终结和Dispose()--处置

    非托管资源:原始的操作系统文件句柄,原始的非托管数据库连接,非托管内存或其他非托管资源。

    Finalize()特性:

    (1)重写Finalize()的唯一原因是,c#类通过PInvoke或复杂的COM互操作性任务使用了非托管资源(典型的情况是通过System.Runtime.InteropServices.Marshal类型定义的各成员)注:PInvoke是平台调用服务。

    (2)object中有finalize方法,但创建的类不能重写此方法,若Overide会报错,只能通过析构函数来达到同样的效果。

    (3)Finalize方法的作用是保证.NET对象能在垃圾回收时清除非托管资源。

    (4)在CLR在托管堆上分配对象时,运行库自动确定该对象是否提供一个自定义的Finalize方法。如果是这样,对象会被标记为可终结的,同时一个指向这个对象的指针被保存在名为终结队列的内部队列中。终结队列是一个由垃圾回收器维护的表,它指向每一个在从堆上删除之前必须被终结的对象。

    注意:Finalize虽然看似手动清除非托管资源,其实还是由垃圾回收器维护,它的最大作用是确保非托管资源一定被释放。

    (5)在结构上重写Finalize是不合法的,因为结构是值类型,不在堆上,Finalize是垃圾回收器调用来清理托管堆的,而结构不在堆上。

    Dispose()特性:

    (1)为了更快更具操作性进行释放,而非让垃圾回收器(即不可预知)来进行,可以使用Dispose,即实现IDispose接口。

    (2)结构和类类型都可以实现IDispose(与重写Finalize不同,Finalize只适用于类类型),因为不是垃圾回收器来调用Dispose方法,而是对象本身释放非托管资源,如Car.Dispose().如果编码时没有调用Dispose方法,以为着非托管资源永远得不到释放。

    (3)如果对象支持IDisposable,总是要对任何直接创建的对象调用Dispose(),即有实现IDisposable接口的类对象都必须调用Dispose方法。应该认为,如果类设计者选择支持Dispose方法,这个类型就需要执行清除工作。记住一点,如果类型实现了IDisposable接口,调用Dispose方法总是正确的。

    (4).net基类库中许多类型都实现IDisposable接口,并使用了Dispose的别名,其中一个别名如IO中的Close方法,等等别名。

    (5)using关键字,实际内部也是实现IDisposable方法,用ildasm.exe查看使用了using的代码的CIL,会发现是用try/finally去包含using中的代码,并且在finally中调用dispose方法。

    相同点:

    都是为了确保非托管资源得到释放。

    不同点:

    (1)finalize由垃圾回收器调用;dispose由对象调用。

    (2)finalize无需担心因为没有调用finalize而使非托管资源得不到释放,而dispose必须手动调用。

    (3)finalize虽然无需担心因为没有调用finalize而使非托管资源得不到释放,但因为由垃圾回收器管理,不能保证立即释放非托管资源;而dispose一调用便释放非托管资源。

    (4)只有类类型才能重写finalize,而结构不能;类和结构都能实现IDispose。

    5.       GC策略

    在传统的堆中,数据结构习惯于使用大块的空闲内存。在其中查找特定大小的内存块是一件很耗时的工作,尤其是当内存中充满碎片的时候。在托管堆中,内存被组制成连续的数组,指针总是巡着已经被使用的内存和未被使用的内存之间的边界移动。当内存被分配的时候,指针只是简单地递增—由此的好处是分配操作的效率得到了很大的提升。

    当对象被分配的时候,它们一开始被放在Gen0中。当Gen0的大小快要达到它的上限的时候,一个只在Gen0中执行的回收操作被触发,由于Gen0的大小很小,因此这将是一个非常快的GC过程。这个GC过程的结果是将Gen0彻底的刷新了一遍。不再使用的对象被释放,确实正被使用的对象整理并移入Gen1中。

    当Gen1的大小随着从Gen0中移入的对象数量的增加而接近它的上限的时候,一个回收动作被触发来在Gen0和Gen1中执行GC过程。如同在Gen0中一样,不再使用的对象被释放,正在被使用的对象被整理并移入下一个Gen中,大部分GC过程的主要目标是Gen0,因为在Gen0中最有可能存在大量的已不再使用的临时对象。对Gen2的回收过程具有很高的开销,并且此过程只有在Gen0和Gen1的GC过程不能释放足够的内存时才会被触发。如果对Gen2的GC过程仍然不能释放足够的内存,那么系统就会抛出outOfMemoryException异常。

    一个带有终止操作的对象被标记未垃圾时,它并不会被立即释放。相反,它会被放置在一个终止队列(finalizationqueue)中,此队列为这个对象建立一个引用,来避免这个对象被回收。后台线程为队列中的每个对象执行它们各自的终止操作,并且将已经执行过终止操作的对象从终止队列中删除。只有那些已经执行过终止操作的对象才会在下一次垃圾回收过程中被从内存中删除。这样做的后果是,等待被终止的对象有可能在它被清除之前,被移入更高的Gen中,从而增加它被清除的延迟时间。

    需要执行终止操作的对象应当实现IDisposable接口,以便客户程序通过此接口快速执行终止动作。IDisposable接口包含一个方法-Dispose,这个被Beta2引入的接口,采用一种在Beta2之前就已经被广泛使用的模式实现。从本质上讲,一个需要终止操作的对象暴露出Dispose方法。这个方法被用来释放外部资源并抑制终止操作。

    6.       参考资料

    http://www.tudou.com/home/diary_v9913437.html

    http://www.cnblogs.com/skywithcloud/archive/2011/08/12/2136789.html

    http://msdn.microsoft.com/zh-cn/magazine/cc163491.aspx

    http://blog.csdn.net/directionofear/article/details/8034133

  • 相关阅读:
    HTML
    HTML协议
    索引原理与慢查询优化
    事务,存储过程
    视图,触发器
    Mysql之单表查询
    剑指offer 面试题4:二维数组中的查找
    剑指offer 面试题3:数组中重复的数字
    剑指offer 面试题2:实现Singleton模式
    剑指offer 面试题1:赋值运算符函数
  • 原文地址:https://www.cnblogs.com/hfclytze/p/3706326.html
Copyright © 2020-2023  润新知