• 关于C#的垃圾回收机制,Finalize和Dispose的区别(自认为很清晰了,有疑问的评论)


    来到个新地方,新学习C#,前面看到C#的垃圾回收,Finalize和Dispose时,总是一知半解,迷迷糊糊。这次好了,前面连续两次面试问到这个问题,脑子里不是很清晰,加上用英文来表达,更是雪上加霜的感觉。

    回来,好好看了相关资料,从网上看,总没有人能说的很清晰,往往很深奥的样子,拿了本<C# language 7.0>,这样中英文结合看,总算清晰了。

    现在主要是找工作,没有时间写详细,先就说个重点:

    1. 垃圾回收器GC负责托管对象的回收。GC通过从应用根对象开始(如全局变量,静态变量等)去访问对象到来判断对象是否能回收,如果无法到达对象,则对像可以被回收。

    而不是通过引用计数法来判断对象是否需要被回收,因为引用计数无法解决一个循环引用的两个对象的回收,他们的引用计数都为1,但实际这两个对象是整个应用程序中的孤岛对象,

    从应用程序根对象开始追踪是无法访问到的,应该被回收。(后续详细解释)

    2. 垃圾回收很耗性能,一般只在分配新对象发现空间不够用时才触发回收。也就是说,何时回收对用户来说是不可控,未知的。

    具体回收时采用的算法是分步回收,即分代回收的方法。其基于统计原理,新产生的对象总是有最大的可能性不被使用而需要回收,比如新调用函数中的新分配的局部对象;而经过几次回收周期中存活下来的对象,则更不可能被回收掉,如全局变量等。

      GC初始化默认每个对象都是需要回收的,回收触发时,通过检测从根对象开始是否能访问到来判断对象是否真的可以回收。GC将对象标签为3代,分别是0,1,和2代对象。0代是还没有经过一次回收检测的对象(没检测过,默认都是可以回收的),1代表示经过一次回收检测后存活下来(即不能回收)的对象,而2代表示经过2次以上检测仍存活的对象。

     GC检测依照顺序,分别检测0代,1代和2代。但是如果检测到0代,回收部分对象后,发现空间已经足够新对象使用,回收检测立刻停止。如果仍然不够,才会继续检测1代对象,直至2代对象。通过这样的方法,避免垃圾回收机制产生作用时的性能消耗。大部分情况下,0代检测就能释放足够空间应付新对象的分配,这样后续的垃圾回收过程就可以避免。

    从回收过程也可以看出,一次回收过程中,哪个对象能被回收,也是不可控,未知的。

    3.垃圾回收器对堆托管对象可以回收,但对非托管对象就需要使用用户手动回收。非托管对象包括文件句柄,socket,数据库连接等。这类对象除了本身是托管外,其内部还包含对操作系统资源申请的对象,这类对象需要使用者主动释放。如文件打开后,需要关闭。

    非托管对象的释放,C#提供了两种方式。一种是使用Finalize(),一种是使用Dispose().

    3.1 Finalize接口是Object的虚保护函数,C#所有的对象都继承自Object,所以都天然可以重写该接口。但C#编译器在此处做了限制,外部用户只能改写析构函数,编译器编译析构函数时,自动会加上Finalize()的调用。因此加上析构函数,就等于重写了Finalize接口。析构函数是垃圾回收器负责调用的,那就意味着Finalize的调用,也是GC负责调用。考虑到上面GC回收对象的机制的特点,我们可以得出Finalize是不确定何时会调用的,虽然肯定的是,其最后始终会被调用。

    Finalize被GC调用的具体过程是:新对象产生时,CLR会监测到Finalize有重写,会将对象建立一个指针引用放入一个Finalization Queue;GC发生作用时,如果该对象被检测出可以回收,则会先将其从Finalization Queue的引用队列移到另外一个叫freachable的引用队列,等待下个回收周期时,在对象空间被释放前,该队列的里对象的Finalize接口被保证调用。因此Finalize比较耗时,至少要两个回收周期才能回收。

    Dispose()来自于接口IDispose,所以只要继承IDispose接口,我们就可以重写Dispose,在里面来释放非托管对象。与Finalize不同的是,Dispose接口的调用,需要使用者自己确定何时调用,因此该调用是明确见效的。使用具有Dispose接口的对象时,一般是采用catch,finally 的形式,在finally调用dispose,这样确保不管出现什么情况dispose可以被调用到。C#利用using关键字简化了该种调用方式。(using db = new DbMyContext()){ ... }这样离开这个using语句块后,CLI会自动调用using新生成对象的Dispose接口。

    3.2 这两者方式各有优缺点。Finalize可以由GC确保最终会调用,非托管对象可以被释放,用户可以不用操心,但又不确定何时会释放。Dispose可以明确立刻释放,但又是不可靠的,因为使用者(程序员自己)可能会忘记显式的调用Dispose接口。

    基于此,最好的办法是利用两者的优点,一起使用。就是说在这两个地方都来释放,确保非托管资源一定释放。因为Fialize的调用是垃圾回收器处理,消耗性能和时间。一旦确定已经调用了Dispose以后,后续的Finalize是可以不用调用的,GC.SuppressFinalize(this)可以完成该功能来,停止Finalize的使用。

    基于此,微软给出了一种推荐的写法:

    public class MyRefObj : IDispose{
    
    private bool disposed = false; // used to identify whether the Dispose() has been called
    
    
    //disposing is used to indicate whethet to explicitly call the managed objects' dispose that you expect to manage intentionally
    
    void CleanUp(bool disposing){
    
      if (!this.disposed)
      {
        if (disposing){
    
        ...  // Clean up the managed resource explicitly, like call the managed objects' Dispose
    
        }
    
        ... // Clean up the unmanaged resource
    
      } 
    
      disposed = true;
    
      }
    
    public void Dispose()
    {
      CleanUp(true);
    
    // Now suppress finalization.
        GC.SuppressFinalize(this);
    }
    
    ~ MyRefObj()
    {
      CleanUp(false);
    }
    }

    说明,析构函数中的CleanUP参数不能为true,因为CleanUp里面会对一些托管资源调用Dispose接口,而托管资源何时被回收是不确定的,因此这些调用的行为是不确定的,所以不能调用。

    基本要点都说完了,后面在补充细节说明。

    1. C#中的Object,Class和reference的关系

    Object是Class的一个实例,而Reference是一个指针。学过C/C++的很好理解,实际其内容是一个内存地址,该内存地址里存放的是实际的对象。

    C#中所有的类的实例化都是通过引用实现,实际是使用new 来实例化一个类。如下举例说明:

    class Program
    {
      static void Main(string[] args)
      {
        Console.WriteLine("***** GC Basics *****");
        // Create a new Car object on the managed heap. We are returned a reference to this object ("refToMyCar").
        Car refToMyCar = new Car("Zippy", 50);
        // The C# dot operator (.) is used to invoke members on the object using our reference variable.
        Console.WriteLine(refToMyCar.ToString());
        Console.ReadLine();
      }
    }

    其中变量refToMyCar实际就是一个引用,一个指针,指向实际的实例对象Car("Zippy", 50)

    在C#中,引用refToMyCar和实例对象Car("Zippy", 50)是存放于不同的内存块中,分别叫做堆栈区和托管内存堆区;

    程序的堆栈区是一个临时数据区,存放函数调用的对象,局部变量等,函数调用完存放于堆栈区的对象的生命期就结束了,每个线程都有固定大小的堆栈。

    而堆是一个全局内存区,所有分配在Managed Heap的对象,需要管理何时释放。在C/C++语言中,这部分数据需要显示的delete和new配套使用,而在C#中,会被.NET CLR来管理,就是说你在托管堆中只管生成新的实例,而不用管该实例占用空间的释放,这由CLR自动管理,实际就是垃圾回收器干的事情。

    2. 垃圾回收器(GC)

     GC是怎样知道一个对象不在使用了呢,这涉及到后面描述的具体算法,简而言之就是GC发现,无法从代码根应用到达的对象。可以以一个不太严谨的解释来理解,就是该分配在堆区的

    对象,已经没有外部引用了。如下一段代码:

    static void MakeACar()
    {
      // If myCar is the only reference to the Car object, it *may* be destroyed when this
      method returns.
      Car myCar = new Car();
    }

    Car的对象在函数外,没有可以引用到,就意味着该对象可以被回收了。

    3.

    C#有两类对象,托管和非托管对象。这个托管指的是对象被谁管理,在.net框架里,就是被CLR托管。非托管对象,指的是一些操作系统提供的资源,比如文件句柄,Socket,数据库连接等。

    这些资源对象的使用,需要像操作系统申请并且使用完后,及时的归还。比如C#的FileStream类,我们使用时有如下一段代码:

    FileInfo finfo = new FileInfo(FilePath);
    //以打开或者写入的形式创建文件流
    using (FileStream fs = finfo.OpenWrite())
    ...{
    //根据上面创建的文件流创建写数据流
    StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.Default);
    //把新的内容写到创建的HTML页面中
    sw.WriteLine(strhtml);
    sw.Flush();
    sw.Close();
    }

    其中,finfo, fs, sw三个分别为FileInfo,FileStream,StreamWriter的对象。

  • 相关阅读:
    Python pip – error: invalid command ‘bdist_wheel’
    css实现文字两端对齐(兼容所有浏览器)
    webpack中的require.context
    node 的path模块中 path.resolve()和path.join()的区别
    react-native项目中遇到的问题
    react-native针对android改变状态栏样式
    createBottomTabNavigator: 怎么在切换tab的时候让页面重新渲染
    当vue页面异步加载的数据想在页面上渲染怎么办
    git分布式版本控制系统
    $router和$route的区别,路由跳转方式name 、 path 和传参方式params 、query的区别
  • 原文地址:https://www.cnblogs.com/dusf/p/11082190.html
Copyright © 2020-2023  润新知