垃圾回收(GC)

1.垃圾回收机制

1.1 GC简介

C#和Java一样是一种系统自动回收释放资源的语言,在C#环境中通过 GC(Garbage Collect)进行系统资源回收,在数据基本类型中介绍到,C#数据类型分为引用类型和值类型, 值类型保存在Stack上,随着函数的执行作用域执行完毕而自动出栈,所以这一类型的资源不是GC所关心的对象。GC垃圾回收主要是指保存在Heap上的资源。

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

  • 首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。
  • 第二,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。

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

1.2 托管资源和非托管资源

GC只释放托管资源,那么什么是托管资源和非托管资源。

  • 托管资源:托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收。
  • 非托管资源:非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标 等。这类资源,调用了内存以外的其他资源,垃圾回收器在清理的时候会调用Object.Finalize()方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。

总结:
托管资源是释放由GC来完成,释放的时间不一定,一般是系统感觉内存吃紧,会进行紧急回收资源。一个对象想成为被回收,首先需要成为垃圾,GC是通过判断对象及其子对象有没有指向有效的引用, 如果没有GC就认为它是垃圾。垃圾回收机制通过一定的算法得到哪些没有被被引用、或者不再调用的资源,当这些垃圾达到一定的数量时,回启动垃圾回收机制,GC回收实际上是调用了析构函数。 垃圾回收机制意味着你不需要担心处理不再需要的对象了,关心的主要是非托管资源的释放。


在C#中存在着Generations的概念,垃圾回收时对象一共有三代 :0,1,2。(频率分配大约为100:10:1)

  • 第 0 代。这是最年轻的代,其中包含短生存期对象。 短生存期对象的一个示例是临时变量。 垃圾回收最常发生在此代中。 新分配的对象构成新一代的对象并且为隐式的第 0 代回收,除非它们是大对象,在这种情况下,它们将进入第 2 代回收中的大对象堆。 大多数对象通过第 0 代中的垃圾回收进行回收,不会保留到下一代。
  • 第 1 代。这一代包含短生存期对象并用作短生存期对象和长生存期对象之间的缓冲区。
  • 第 2 代。这一代包含长生存期对象。 长生存期对象的一个示例是服务器应用程序中的一个包含在进程期间处于活动状态的静态数据的对象。 每一代都有自己的内存预算,空间不足的时候会调用垃圾回收。

当条件得到满足时,垃圾回收将在特定代上发生。 回收某个代意味着回收此代中的对象及其所有更年轻的代。 第 2 代垃圾回收也称为完整垃圾回收,因为它回收所有代上的所有对象(即,托管堆中的所有对象)。 

垃圾回收中未回收的对象也称为幸存者,并会被提升到下一代。 在第 0 代垃圾回收中幸存的对象将被提升到第 1 代;在第 1 代垃圾回收中幸存的对象将被提升到第 2 代;而在第 2 代垃圾回收中幸存的对象将仍为第 2 代。 

当垃圾回收器检测到某个代中的幸存率很高时,它会增加该代的分配阈值,因此下一次回收将会获取一个非常大的回收内存。 CLR 会在以下两个优先级别之前进行平衡:不允许应用程序的工作集获取太大内存以及不允许垃圾回收花费太多时间。 

GC进行垃圾回收是系统决定的,下面是进行强制回收的执行代码(非特殊情况下不要使用此方法,会影响系统效率,削弱垃圾回收器中优化引擎的作用,而垃圾回收器可以确定运行垃圾回收的最佳时间)。

//对所有代进行垃圾回收。
GC.Collect();
//对指定的代进行垃圾回收。
GC.Collect(int generation); 
//强制在 System.GCCollectionMode 值所指定的时间对零代到指定代进行垃圾回收。
GC.Collect(int generation, GCCollectionMode mode); 

1.3 非托管资源的释放

在定义一个类时,可以使用两种不同的机制来释放非托管资源,这两种机制有时候通常放在一起使用

1.3.1 声明析构函数(终结器)

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

public class AA
{
    ~AA()
    {
        //析构函数语法
    }
}

析构函数会被编辑器翻译成下面的代码:

protected override void Finalize()
{
    try
    {
        // Cleanup statements...     
    }
    finally
    {
        base.Finalize();
    }
}

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

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

综上所述,建议是不要实现其析构函数,这将大大降低程序的性能。

1.3.2 在类中实现 System.IDisposable 接口

实现IDisposable接口来显示释放系统资源

class Test : IDisposable 
{
    #region IDisposable Support
    private bool disposedValue = false; // 要检测冗余调用
    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // TODO: 释放托管状态(托管对象)。
            }
            // TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
            // TODO: 将大型字段设置为 null。
            disposedValue = true;
        }
    }
    // TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
    ~Test()
    {
        // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
        Dispose(false);
    }
    // 添加此代码以正确实现可处置模式。
    public void Dispose()
    {
        // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
        Dispose(true);
        // TODO: 如果在以上内容中替代了终结器,则取消注释以下行。
        // GC.SuppressFinalize(this); 
    }
    #endregion
}
View Code

① 当我们显示调用Dispose()方法以后,会执行释放非托管资源的操作,然后disposedValue会为Flase,所以我们多次调用也没有关系。Dispose()调用执行完以后,执行 GC.SuppressFinalize(this)(告诉GC不再执行终结器操作)代码
② 如果我们不调用 Diapose()方法,系统会调用使用终结器操作,最后也是释放非托管资源。

从例子可以看出,对于手动回收(disposing为true),除了非托管资源,还可以通知其他托管对象Dispose(),因为这时候内部的托管对象肯定没回收。而到了自动回收,就不能通知其他托管对象了,因为垃圾回收可能已经把他们回收了,而且垃圾回收会自动回收他们,也不用你通知了。

总结
当释放非托管资源时我们应该显式的去实现Dipose()方法或者Close()方法,但是万一我们忘记显式去调用方法,此时还有一条退路,CLR会自动调用Finalize()方法, 很显然调用Finalize()方法会大大降低程序的性能,上述释放模式关键的一点是通过手动释放调用Dispose()方法可以阻止Finalize()方法的调用,换言之,上述通过手动释放既释放了非托管资源又加快了程序运行的速度,毫无疑问,这是一种完美的解决方案。
① Finalize是系统决定执行的,我们无法干涉。Dispose是可以我们调用来释放的。
② Finalize只能释放非托管资源,Dispose既可以释放托管资源也可以释放非托管资源。

2.详细参考

博客园里写得好的 GC 详解太多了,自己也不想去写了,下面有几个很好的来自 玉开 的参考,从回收的原理、算法到对大对象的处理。

.Net 垃圾回收机制原理(一)

.Net 垃圾回收机制原理(二)

.Net 垃圾回收和大对象处理

让垃圾回收器高效工作(一)

让垃圾回收器高效工作(二)

让垃圾回收器高效工作(三)

让垃圾回收器高效工作(四) 

来自 .NET开发菜鸟 的对CLR、内存分配和垃圾回收的讲解

来自 白细胞 的对CLR和.Net对象生存周期的讲解

原文地址:https://www.cnblogs.com/jizhiqiliao/p/9868749.html