C# 关于GC垃圾回收的过程

一、数据类型

C#中的数据类型分为值类型 (Value type) 和引用类型(reference type),

值  类 型: 所有的值类型都集成自 System.ValueType 上,除非加声明?否则不可为null,
保存在 堆栈(Stack,先进后出)上,常见的值类型有:整形、浮点型、bool、枚举等。

引用类型:所有的引用类型都继承自System.Object 上,引用类型保存在 托管堆(Head,先进先出)上,
常见的类型有:数组、字符串、接口、委托、object等。

拆箱和装箱:引用类型和值类型的相互转换叫做拆装箱操作。

拆箱:拆箱就是将一个引用型对象转换成任意值型!比如:

int i=0;
System.Object obj=i;
int j=(int)obj;
 装箱:装箱就是隐式的将一个值型转换为引用型对象。比如:

int i=0;
Syste.Object obj=i;

二、析构函数

	首先介绍一下析构函数,C#的析构函数不像是C++,因为C#的垃圾回收是自动的,不是自己手动释放的
,所以很少用得到。
  当引用类型的堆内存被回收时,会调用该函数(注意这个不是将其置为null的时候调用,
而是系统自动进行回收的时候调用的,或者你强制进行一次回收)
  对于需要手动管理内存的语言(比如C++),需要在析构函数中做一些内存回收处理
但是C#中存在自动垃圾回收机制GC
所以我们几乎不会怎么使用析构函数。除非你想在某一个对象被垃圾回收时,做一些特殊处理

三、 分类

1、简介

  • C#中和Java一样是一种系统自动回收释放资源的语言,在C#环境中通过 GC(Garbage Collect)
    进行系统资源回收,在数据基本类型中介绍到,C#数据类型分为引用类型和值类型,

  • 值类型保存在Stack上,随着函数的执行作用域执行完毕而自动出栈,所以这一类型的资源不是GC所关心 对象。GC垃圾回收主要是是指保存在Heap上的资源。

    NET的GC机制有这样两个问题:

  • 首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。

  • 第二,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。
    GC并不是实时性的,这会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托管资源。使用using语句可以简化资源管理。

2、托管资源和非托管资源

上面介绍到,GC只释放托管资源,那么什么是托管资源和费托管资源。

  • 托管资源 : 托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收。

  • 非托管资源:非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标 等。这类资源,

垃圾回收器在清理的时候会调用Object.Finalize()方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。

3、托管资源的回收方法

总结:托管资源是释放由GC来完成,释放的时间不一定,一般是系统感觉内存吃紧,会进行紧急回收资源。一个对象想成为被回收,首先需要成为垃圾,GC是通过判断对象及其子对象有没有指向有效的引用,

如果没有GC就认为它是垃圾。垃圾回收机制通过一定的算法得到哪些没有被被引用、或者不再调用的资源,当这些垃圾达到一定的数量时,回启动垃圾回收机制,GC回收实际上是调用了析构函数。
垃圾回收机制意味着你不需要担心处理不再需要的对象了。咱们关心的主要是非托管资源的释放。
垃圾回收时对象一共有三代 :0,1,2。每一代都有自己的内存预算,空间不足的时候会调用垃圾回收。为了提高性能都是按代回收,第0代超预算之后就回收第0代的对象,而存活下来的对象就提升为第1代,

依次类推,而往往经过多次0代的垃圾回收才能回收一次第1代。
例如:
有 a,b,c,d 四个对象
首选将abc放入0代内存,这个时候,abc并不是相邻的,
如果这个时候,0代内存满了,就会去查找这个里面有没有断开引用的对象,有的话,就清除,另外将没有被清除的都放入到1代内存中,这个时候就会进行一个排序,abc就是相邻的,然后再次新建的内存就会在空的0代中,这样来回,直到1代满了,那么他就会将不需要被回收的都放入到2代中,然后同时清除自己和自己的上一代(0代)。由此,可知道2代是存活时间最久的,最稳定的,最不容易被清除的,我们通常不自己主动进行垃圾回收,一般都是在loading的时候进行。

4、非托管资源的回收

非托管资源的回收是需要手动进行的
有两种回收机制:

1、声明析构函数(终结器),作为类的成员

析构函数是资源创建以后被系统回收的时候执行的操作,垃圾回收器在回收对象之前会调用析构函数,所以在函数代码块中可以写释放非托管资源的代码。析构函数没有返回值,没有参数,没有修饰符。

public class AA
{
     ~AA()
     {
         //析构函数语法
     }
 }
//析构函数会被编辑器翻译成下面的代码:
protected override void Finalize()
 {
        try
        {
            // Cleanup statements...     
        }
        finally
        {
            base.Finalize();//可以看到他会自动调用Finalize方法
        }
    }
最终析构函数会被翻译成上面的代码块,重写基类的Finalize()方法,然后最终调用 Base.Finalize()方法。

注意!大量的使用析构函数会影响效率!带有析构函数的对象会被系统执行两次才会被释放掉。
GC执行释放资源时,没有析构函数的资源会被直接释放掉,假如目标对象有析构函数,
会被先放进一个叫做“终结队列”的项中去,然后系统调用另一个高优先级线程来执行 Finalize()方法,
GC继续回收其它对象。等方法执行完以后会将对象从终结队列中清除出去,此时对象才是真正意义上的垃圾。
等GC执行资源回收的时候,才回释放掉终结队列里面的对象。注意这里是两个线程进行执行,而不是一个!


总结:
托管堆中内存的释放和析构函数的执行分别属于两个不同的线程
带有析构函数的对象其生命周期会变长,由上知会进行两次垃圾回收处理才能被释放,如此一来将导致程序性能的下降。
若一个对象引用了其他对象时,当此对象不能够被释放时,则其引用对象也就无法进行内存的释放,也就意味着带有析构函数的对象和其引用对象将从第0代提升到第一代,毫无疑问将影响程序的性能
综上所述,建议是不要实现其析构函数,这将大大降低程序的性能。

2、在类中实现 System.IDisposable 接口

所以对于包含非托管资源的对象,最好及时的调用Dispose()方法来回收资源,而不是依赖垃圾回收器。
在一个包含非托管资源的类中,关于资源释放的标准做法是:

  • 继承IDisposable接口;
  • 实现Dispose()方法,在其中释放托管资源和非托管资源,并将对象本身从垃圾回收器中移除(垃圾回收器不在回收此资源);
  • 实现类析构函数,在其中释放非托管资源。
Public class BaseResource:IDisposable
   {
      PrivateIntPtr handle; // 句柄,属于非托管资源
      PrivateComponet comp; // 组件,托管资源
      Privateboo isDisposed = false; // 是否已释放资源的标志
       
      PublicBaseResource
      {
      }
        
       //实现接口方法
       //由类的使用者,在外部显示调用,释放类资源
       Public void Dispose()
       {
           Dispose(true);// 释放托管和非托管资源
                           
          //将对象从垃圾回收器链表中移除,
         // 从而在垃圾回收器工作时,只释放托管资源,而不执行此对象的析构函数,这样就可以保证只有一个线程去运行,而且下面Dispose中也能避免二次释放,就节省了资源,,提高了效率

            GC.SuppressFinalize(this);
         }
        
         //由垃圾回收器调用,释放非托管资源

       ~BaseResource()
        {
           Dispose(false);// 释放非托管资源
        }
        
     //参数为true表示释放所有资源,只能由使用者调用
    //参数为false表示释放非托管资源,只能由垃圾回收器自动调用
   //如果子类有自己的非托管资源,可以重载这个函数,添加自己的非托管资源的释放
  //但是要记住,重载此函数必须保证调用基类的版本,以保证基类的资源正常释放
    Protectedvirtual void Dispose(bool disposing)
    {
       If(!this.disposed)// 如果资源未释放 这个判断主要用了防止对象被多次释放
         {
            If(disposing)
            {
               Comp.Dispose();// 释放托管资源
             }
                           
           closeHandle(handle);// 释放非托管资源
           handle= IntPtr.Zero;
           }
          this.disposed= true; // 标识此对象已释放
      }
  }

垃圾回收机制


    #region 垃圾回收机制
    //垃圾回收,英文简写GC(Garbage Collector)
    //垃圾回收的过程是在遍历堆(Heap)上动态分配的所有对象
    //通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用
    //所谓的垃圾就是没有被任何变量,对象引用的内容
    //垃圾就需要被回收释放

    //垃圾回收有很多种算法,比如
    //引用计数(Reference Counting)
    //标记清除(Mark Sweep)
    //标记整理(Mark Compact)
    //复制集合(Copy Collection)

    //注意:
    //GC只负责堆(Heap)内存的垃圾回收
    //引用类型都是存在堆(Heap)中的,所以它的分配和释放都通过垃圾回收机制来管理

    //栈(Stack)上的内存是由系统自动管理的
    //值类型在栈(Stack)中分配内存的,他们有自己的生命周期,不用对他们进行管理,会自动分配和释放

    //C#中内存回收机制的大概原理
    //0代内存     1代内存     2代内存
    //代的概念:
    //代是垃圾回收机制使用的一种算法(分代算法)
    //新分配的对象都会被配置在第0代内存中
    //每次分配都可能会进行垃圾回收以释放内存(0代内存满时) 

    //在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步
    //1.标记对象 从根(静态字段、方法参数)开始检查引用对象,标记后为可达对象,未标记为不可达对象
    //  不可达对象就认为是垃圾
    //2.搬迁对象压缩堆  (挂起执行托管代码线程) 释放未标记的对象 搬迁可达对象 修改引用地址

    //大对象总被认为是第二代内存  目的是减少性能损耗,提高性能
    //不会对大对象进行搬迁压缩  85000字节(83kb)以上的对象为大对象
  
    #endregion
	
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("构造函数和析构函数");

            Person p = new Person(18,"唐老狮");
            Console.WriteLine(p.age);

            p = null;

            //手动触发垃圾回收的方法 
            //一般情况下 我们不会频繁调用
            //都是在 Loading过场景时 才调用
            //对所有代进行垃圾回收。
			GC.Collect();
			//对指定的代进行垃圾回收。
			GC.Collect(int generation); 
			//强制在 System.GCCollectionMode 值所指定的时间对零代到指定代进行垃圾回收。
			GC.Collect(int generation, GCCollectionMode mode); 
        }
    }

注意事项

GC注意事项:

1、只管理内存,非托管资源,如文件句柄,GDI资源,数据库连接等还需要用户去管理。

2、循环引用,网状结构等的实现会变得简单。GC的标志-压缩算法能有效的检测这些关系,并将不再被引用的网状结构整体删除。

3、GC通过从程序的根对象开始遍历来检测一个对象是否可被其他对象访问,而不是用类似于COM中的引用计数方法。

4、GC在一个独立的线程中运行来删除不再被引用的内存。

5、GC每次运行时会压缩托管堆。

6、你必须对非托管资源的释放负责。可以通过在类型中定义Finalizer来保证资源得到释放。

7、对象的Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间。注意并非和C++中一样在对象超出声明周期时立即执行析构函数

8、Finalizer的使用有性能上的代价。需要Finalization的对象不会立即被清除,而需要先执行Finalizer.Finalizer,不是在GC执行的线程被调用。GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行所有这些Finalizer,而GC线程继续去删除其他待回收的对象。在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。

9、.NET GC使用"代"(generations)的概念来优化性能。代帮助GC更迅速的识别那些最可能成为垃圾的对象。在上次执行完垃圾回收后新创建的对象为第0代对象。经历了一次GC周期的对象为第1代对象。经历了两次或更多的GC周期的对象为第2代对象。代的作用是为了区分局部变量和需要在应用程序生存周期中一直存活的对象。大部分第0代对象是局部变量。成员变量和全局变量很快变成第1代对象并最终成为第2代对象。

10、GC对不同代的对象执行不同的检查策略以优化性能。每个GC周期都会检查第0代对象。大约1/10的GC周期检查第0代和第1代对象。大约1/100的GC周期检查所有的对象。重新思考Finalization的代价:需要Finalization的对象可能比不需要Finalization在内存中停留额外9个GC周期。如果此时它还没有被Finalize,就变成第2代对象,从而在内存中停留更长时间。

参考:参考1
参考2
参考3

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值