浅谈.NET垃圾回收-Garbage Collector

什么是GC

GC(Garbage Collector),垃圾内存收集,它以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的、哪些仍需要被使用。已经不再被应用程序的root或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收,也就是释放或者销毁对象所占用的内存。

实现GC有多种算法。比较常见的算法有Reference Counting,Mark Sweep,Copy Collection等。目前主流的虚拟系统.NET CLR,Java VM和Rotor都是采用的Mark Sweep算法。

.NET的GC机制只能自动回收托管资源,对于非托管资源不能自动回收:如图像对象,数据库连接,文件句柄,网络连接,COM封装器对象(COM组件)等等。在.net的类中,主要有以下各类:OleDBDataReader,StreamWriter,ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,mage,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip 等。

此外,由于GC算法设计自身的特点,GC清理对象和释放垃圾内存并不是实时的,程序人员并不知道具体的时间,因此可能会造成本身不再使用的对象内存并没有释放,而程序在其它地方在此对这些非托管资源进行了访问,此时就有可能发生错误。并且这种错误是随机的,有时候GC正好回收的对象的内存,程序运行正常,有时候GC还没有来得及释放这段内存,程序就会运行异常,从而造成系统运行的不确定性。

对于非托管对象,.net为程序员提供了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托管资源,也可以使用using语句简化资源对象的管理和操作。

GC中用到的几个函数

来介绍一下GC中用到的几个函数:
GC.SuppressFinalize(this);//请求公共语言运行时不要调用指定对象的终结器。
GC.GetTotalMemory(false);//检索当前认为要分配的字节数。一个参数,指示此方法是否可以等待较短间隔再返回,以便系统回收垃圾和终结对象。
GC.Collect();//强制对所有代进行即时垃圾回收。

GC运行机制

大家都知道GC是一个后台线程,他会周期性的查找对象,然后调用Finalize()方法去销毁他,开发人员继承IDispose接口,调用Dispose方法,销毁了对象,而GC并不知道。GC依然会调用Finalize()方法,而在.NET中Object.Finalize()方法是无法重载的,所以我们可以使用析构函数来阻止重复的释放。我们调用完Dispose方法后,还有调用GC.SuppressFinalize(this)方法来告诉GC,不要再调用这些对象的Finalize()方法了。

在垃圾回收时尽量避免使用finallize来回收资源,这样会造成两车垃圾回收,影响效率。

垃圾回收器使用名为“终止队列”的内部结构跟踪具有Finalize方法的对象。当应用程序创建具有Finalize方法的对象时,垃圾回收器都在终止队列中放置一个指向该对象的项。托管堆中所有需要在垃圾回收器回收其内存之前调用它们的终止代码的对象都在终止队列中含有项。(实现Finalize方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用Finalize方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。这时,它不能回收具有终结器的不可访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。垃圾回收器为此列表中的对象调用Finalize方法,然后,将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。

垃圾收集器收集垃圾对象的规则

GC执行垃圾收集是一个非常复杂的算法,大概可以描述成这样:
CLR按对象在内存中的存活的时间长短,来收集对象。时间最短的被分配到第0代,最长的被分配到第2代,一共就3代。

一般第0代的对象都是较小的对象,第2代的对象都是较大的对象,第0代对象GC收集时间最短(毫秒级别),第2代的对象GC收集时间最长。当程序需要内存时(或者程序空闲的时),GC会先收集第0代的对象,收集完之后发现释放的内存仍然不够用,GC就会去收集第1代,第2代对象。(一般情况是按这个顺序收集的)。

如果GC运行过几次之后内存空间依然不够用,那么就抛出了OutOfMemoryException异常。GC运行几次之后,第0代的对象仍然存在,那么CLR会把这些对象移动到第1代,第1代的对象也是这样。

如果GC发现上一次收集了很多对象,释放了很大的内存,那么它就会尽快执行第二次回收。如果它频繁的回收,但释放的内存不多,那么它就会减慢回收的频率。所以,尽量不要调用GC.Collect()这样会破坏GC现有的执行策略。除非你对你的应用程序内存使用情况非常了解,你知道何时会产生大量的垃圾,那么你可以手动干预垃圾收集器的工作。

关于GC.Collect(),MSDN上官方文档中有如下一段话:
垃圾回收GC类提供GC.Collect方法,您可以使用该方法让应用程序在一定程度上直接控制垃圾回收器。通常情况下,您应该避免调用任何回收方法,让垃圾回收器独立运行。在大多数情况下,垃圾回收器在确定执行回收的最佳时机方面更有优势。但是,在某些不常发生的情况下,强制回收可以提高应用程序的性能。当应用程序代码中某个确定的点上使用的内存量大量减少时,在这种情况下使用GC.Collect方法可能比较合适。例如,应用程序可能使用引用大量非托管资源的文档。当您的应用程序关闭该文档时,您完全知道已经不再需要文档曾使用的资源了。出于性能的原因,一次全部释放这些资源很有意义。

在垃圾回收器执行回收之前,它会挂起当前正在执行的所有线程。如果不必要地多次调用GC.Collect,这可能会造成性能问题。您还应该注意不要将调用GC.Collect的代码放置在程序中用户可以经常调用的点上。这可能会削弱垃圾回收器中优化引擎的作用,而垃圾回收器可以确定运行垃圾回收的最佳时间。

测试和试验

GC回收对象的机制和不确定性

private void button1_Click(object sender,EventArgs e)
{
    AA a=new AA();
    AA b=new AA();
    AA c=new AA();
    AA d=new AA();
}
public class AA{}

在讲这个例子之前,要明白什么被称之为垃圾,垃圾就是一个内存区域,没有被任何引用指向,或者不再会被用到。哪么在第一次点击按钮的时候会生成4个对象,第二次点击按钮的时候也会生成4个对象,但是第一次生成的4个对象就已经是垃圾了,因为,第一次生成的4个对象随着button1_Click函数的结束而不会再被调用(或者说不能再被调用),哪么这个时候GC就会来回收吗?不是的!我说了GC是随机的,哪么你只管点你的,不一会GC就会来回收的(这里我们可以认为,内存中存在一定数量的垃圾之后,GC会来),要证明GC来过我们把AA类改成:

public class AA
{
    ~AA()
    {
        MessageBox.Show("析构函数被执行了");
    }
}

要明白,GC清理垃圾,实际上是调用析构函数,但是这些代码是托管代码(因为里面没有涉及到Steam,Connection等)所以在析构函数中,我们可以只写一个MsgBox来证明刚的想法;这个时候,运行你的程序,一直点击按钮,不一会就会出现一大堆的“析构函数被执行了”

好了,然后让我们看看能不能改变GC这种为所欲为的天性,答案是可以的,我们可以通过调用GC.Collect();来强制GC进行垃圾回收,哪么button1_Click修改如下:

private void button1_Click(object sender,EventArgs e)
{
    AA a=new AA();
    AA b=new AA();
    AA c=new AA();
    AA d=new AA();
    GC.Collect();
}

哪么在点击第一次按钮的时候,生成四个对象,然后强制垃圾回收,这个时候,会回收吗?当然不会,因为,这四个对象还在执行中(方法还没结束),当点第二次按钮的时候,会出现四次”析构函数被执行了”,这是在释放第一次点击按钮的四个对象,然后以后每次点击都会出现四次”析构函数被执行了”,哪么最后一次的对象什么时候释放的,在关闭程序的时候释放(因为关闭程序要释放所有的内存)。

回收非托管资源的方法

对于非托管代码,假设有如下一个类:

    public class AA
    {
        FileStream fs=new FileStream("D://a.txt",FileMode.Open);
        ~AA()
        {
            MessageBox.Show("析构函数被执行了");
        }
    }

private void button1_Click(object sender,EventArgs e)
{
    AA a=new AA();
}

点击第二次的时候就会报错,原因是一个文件只能创建一个连接。那么一定要释放掉第一个资源,才可以进行第二次的连接。如果采用GC.Collect(),来强制释放闲置的资源,修改代码如下:

private void button1_Click(object sender,EventArgs e)
{
    GC.Collect();
    AA a=new AA();
}

可以看到,第二次点按钮的时候,确实出现了“析构函数被执行了“,但是程序仍然错了,原因前面我说过,因为Stream不是托管代码,所以C#不能帮我们回收,哪怎么办?自己写一个Dispose方法;去释放我们的内存。代码如下:

public class AA:IDisposable
{
    FileStream fs=new File Stream("D://a.txt",FileMode.Open);
    ~AA()
    {
    MessageBox.Show("析构函数被执行了");
    }
    #regionIDisposable成员
    public void Dispose()
    {
        fs.Dispose();
        MessageBox.Show("dispose执行了");
    }
    #endregion
}

继承IDisposable接口以后会有一个Dispose方法(当然了,你不想继承也可以,但是接口给我们提供一种规则,你不愿意遵守这个规则,就永远无法融入整个团队,你的代码只有你一个人能看懂),好了闲话不说,这样一来我们的button1_Click改为

private void button1_Click(objectsender,EventArgse)
{
    AA a=new AA();
    a.Dispose();
}

我们每次点击之后,都会发现执行了“dispose执行了”,在关闭程序的时候仍然执行了“析构函数被执行了”这意味了,GC还是工作了,哪么如果程序改为:

private void button1_Click(objectsender,EventArgse)
{
    AAa=newAA();
    a.Dispose();
    GC.Collect();
}

每次都既有“dispose执行了又有”“析构函数被执行了”,这意味着GC又来捣乱了,哪么像这样包含Stream connection的对象,就不用GC来清理了,只需要我们加上最后一句话GC.SuppressFinalize(this)来告诉GC,让它不用再调用对象的析构函数中。那么改写后的AA的dispose方法如下:

public void Dispose()
{
    fs.Dispose();
    MessageBox.Show("dispose执行了");
    GC.SuppressFinalize(this);
}

参考资料

http://blog.csdn.net/xmsheji/article/details/5452914?
http://blog.csdn.net/libaineu2004/article/details/40055149?
http://blog.csdn.net/a_long_/article/details/52029092?
http://blog.csdn.net/luyifeiniu/article/details/1968802
https://www.cnblogs.com/yunfeifei/p/3995342.html
http://stackoverflow.com/questions/538060/proper-use-of-the-idisposable-interface
https://stackoverflow.com/questions/1149197/gc-collect
http://blog.csdn.net/aoshilang2249/article/details/38581101

你可能感兴趣的:(编程语言)