• 菜鸟之旅——.NET垃圾回收机制


      .NET的垃圾回收机制是一个非常强大的功能,尽管我们很少主动使用,但它一直在默默的在后台运行,我们仍需要意识到它的存在,了解它,做出更高效的.NET应用程序;下面我分享一下我对于垃圾回收机制(GC)的学习心得。

    GC的必要性

      我们知道程序会需要向内存堆使用new请求内存,然后将请求的内存初始化并使用,使用完毕之后,变清理资源和释放内存,等待别的程序来请求使用;对内存资源的管理方式,现在存在这么几种管理方式:

      1、手动管理:C、C++

      2、计数管理:COM

      3、自动管理:.NET、JAVA、PHP

      现在的高级语言基本上都实现了自动管理内存,这是因为手动管理内存会因为人为的原因产生以下问题:

      1、开发人员忘记释放请求的内存,造成内存泄漏,若是内存泄露过多,则可能会造成内存溢出,导致程序无法运行;

      2、应用程序访问已释放的内存,造成数据读取错误。

      由此可见,手动去管理堆里面的内存可靠程度,会因开发人员的不同而不同,在C++因指针而出现的问题可不少;而且易出现Bug等乱七八糟的问题,影响系统稳定性,所以自动化管理内存是必要的。

    GC的工作原理

     通用概念

      回收时机

      当应用程序分配新的对象,GC的代的预算大小已经达到阈值,比如GC的第0代已满;

      代码主动显式调用System.GC.Collect();

      其他特殊情况,比如,windows报告内存不足、CLR卸载AppDomain、CLR关闭,甚至某些极端情况下系统参数设置改变也可能导致GC回收。

      应用程序根

      应用程序根(application root):根(root)就是一个存储位置其中保存着对托管堆上一个对象的引用,根可以属性下面任何一个类别

    • 全局对象和静态对象的引用
    • 应用程序代码库中局部对象的引用
    • 传递进一个方法的对象参数的引用
    • 等待被终结(finalize,后面介绍)对象的引用
    • 任何引用对象的CPU寄存器

      代

      垃圾回收器将托管堆(heap)里面的对象划分为3个代(一般为3代),可以使用GC.MaxGeneration()方法来进行查询当前系统所支持的最大代数:

      1、G0 小对象(Size<85000Byte):新分配的小于85000字节的对象

      2、G1:在GC中幸存下来的G0对象

      3、G2:大对象(Size>=85000Byte);在GC中幸存下来的G1对象

      当一个对象被new的时候,它的代为0,经过一次回收之后,若该对象没有被回收,则代上升,变为1,若每次回收都幸存下来,则代都会上升,最大代为操作系统所支持的最大代。

      因为将对象以代划分,并且可以单独回收某一个世代,避免回收整个托管堆,提升性能。一个基于代的垃圾回收器有一下特点:

      1、对象越新,生存期越短;

      2、对象越老,生存期越长;

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

     工作过程

      标记对象

      在垃圾回收的第一步就是标记对象:垃圾回收器会认为托管堆中的所有对象都是垃圾,然后垃圾回收器会去检查所有的应用程序根,遍历每个根所引用到的对象,将其标记为活动的(live ),所有的根对象都检查完之后,有标记的对象就是可达对象,未标记的对象就是不可达对象,不可达对象就是回收的目标。

      弱引用对象则不在考虑范围之内,所以一定会被回收掉的。

      销毁对象,释放内存

      在经过第一步的对象筛选之后,回收没有被引用的对象,就是不可达对象,GC调用对象默认的终结器Finalize(),销毁对象之后,将内存也释放掉。

      同时,还存在引用的对象,就是可达对象的世代变为下一个世代。

      压缩堆内存

      经过第二步的销毁对象和释放内存之后,幸存下来的对象在堆中的排列可能是不连续的,这时在堆中存在非常多的内存碎片,程序在new对象的时候都是请求一段连续的内存,则内存碎片可能就无法再次利用(虽然没有被使用),造成内存资源的浪费,所以垃圾回收的最后一步就是压缩内存:将垃圾回收后幸存的对象移动到一起,并且将各个对象的引用更新到对象新的位置上,保证对象引用的正确性。

      注:从这里看得出,在压缩堆内存的时候,所有相关线程必须暂停,因为压缩时不能保证对象引用的正确性,所以在垃圾回收的时候,GC会劫持所有相关线程,在回收完毕之后,被劫持的线程才会正常工作,所以垃圾回收势必会影响一定的性能,所以慎用System.GC.Collect()。

     Finalize()与Dispose()

      上面说到,GC在回收对象的时候是调用对象的终结器Finalize()来实现的,那么,就简单的总结一下Finalize()与Dispose()吧:

      1、调用者:

        Finalize只能由GC调用

        Dispose由开发人员显示调用,也可以使用use区块,在程序离开区块使自动调用Dispose方法

      2、调用时机:

        Finalize由于是GC调用的,所以调用时机是垃圾回收的时候调用,时机不确定

        Dispose由于是显示调用,所以调用时机是确定的,在调用方法的时候就调用了

      3、目的:

        这里的目的主要说是Dispose出现的目的;

        首先是.NET存在托管资源和非托管资源,一般来说,非托管资源数量有限,比较珍贵,在使用完毕之后,希望能够释放掉,那么将释放非托管资源的方法写到终结器Finalize里面也是可以的,但是由于Finalize的调用时机不确定,导致释放资源不及时,那么有限的非托管资源很快就被占用完毕,所以,为了能够及时的释放掉这类资源,我们需要能够显示调用的方法,这就是Dispose。

        Finalize主要是为了GC释放托管资源和销毁对象,释放内存

        Dispose主要是为了释放托管和非托管资源和销毁对象,释放内存

      注:不必担心资源的重复释放问题,就算是重复释放,.NET也做好了相应措施来处理,不会抛出异常。

        下面贴一个MSDN推荐的标准的Dispose实现方式

        class Class : IDisposable
        {
            // 标识:是否释放托管资源
            private bool disposed = false;
    
            // 显示调用的方法
            public void Dispose()
            {
                Dispose(true);
                // 将对象从垃圾回收器链表中移除,
                // 从而在垃圾回收器工作时,只释放托管资源,而不执行此对象的析构函数
                GC.SuppressFinalize(this);
            }
    
            // 受保护的释放资源方法
            protected virtual void Dispose(bool disposing)
            {
                if (!this.disposed)
                {
                    if (disposing)
                    {
                        // 此处写释放托管资源的方法
                    }
                    disposed = true;
    
                    // 此处写释放非托管资源的方法
                }
            }
            ~Class()
            {
                // 这里是防止忘记显示调用Dispose(),在GC进行垃圾回收的时候进行释放非托管资源
                Dispose(false);
            }
        }
    View Code

    总结

      GC所带来的便利是不言而喻的,但是这是付出一定的系统性能来实现的:在垃圾回收的时候GC会劫持所有相关的线程,并且会有一定的时空开销,所以在平时开发过程中注意一些良好的开发习惯可能会对GC有一些积极的影响。

      1、尽量不要new很大的对象,大对象(>=85000Byte)直接归为G2代,GC回收算法从来不对大对象堆(LOH)进行内存压缩整理,移动大对象将会消耗更多的CPU时间,也更容易造成内存碎片。这里也可以将大对象或者生命周期长的对象进行池化。

      2、不要频繁的new生命周期短的小对象,这可能会导致频繁的垃圾回收,这里可以考虑使用结构体放在栈中来代替,或者也可以使用对象池化来优化。

      3、不推荐使用对象池化的解决方案,它比较笨重和容易出错,设计一个高性能稳定的对象池并不容易。

      4、降低对象之间的纵向深度,GC在回收过程中,会先顺着根来进行对象遍历和标记,减少深度可以加快遍历速度;若系统中各个类之间的关系错综复杂,那么考虑一下设计方案是否合理。

      当然注意的地方还有不少,最后贴一篇博客,这里介绍了如何编写高性能的.NET代码,其中的GC介绍非常详细:

      [翻译]【目录】编写高性能 .NET 代码

       

  • 相关阅读:
    JSP脚本指令
    JSP编译指令——page、include
    c++基础(三):多态
    c++基础(一):数据类型和结构
    c++基础(二):成员he派生类
    python小算法(二)
    python的内存管理
    初识java之Mina(一)
    python的小爬虫的基本写法
    python小算法(一)
  • 原文地址:https://www.cnblogs.com/nbclw/p/8458885.html
Copyright © 2020-2023  润新知