.NET中的垃圾回收机制

在.NET中,应用程序的内存分成两个部分,一部分为托管内存,由.NET运行时(CLR)进行管理;另外一部分为非托管内存(通过Win32 API或者Marshal.AllocateXXX方法分配),需要程序员自己进行管理。托管内存都在堆(Heap)中进行分配,在这个堆中,.NET通过不同的“代”(Generation)来组织对象。托管堆共分成三代:

  • 第0代(Generation 0):这一代是最年轻的,其中包含一些短生存期的对象,比如方法中的局部变量等。这一代的对象会被回收的比较频繁,在新建对象的时候,一般会创建在这一代中。
  • 第1代(Generation 1):这一代中的对象也是短生存期的,但只是作为一个过渡。一些较大的对象,在创建的时候也会在这一代。
  • 第2代(Generation 2):这一代中的对象都是长生存期的,比如静态类、静态成员等。他们被回收的概率会很低,所以在编写程序的时候应该尽量避免使用静态成员。

在每一代中的执行(通过显式调用GC.Collect方法)一次内存回收时,如果有对象无法被回收,它们将被移到下一代中。比如第1代中定义了一个对象,它在一次回收操作中无法被回收,那它就会被移到第1代中去。这样的好处就是移动之后的对象下次不会再被频繁的尝试去回收。因为越是后代中的对象,被回收的频率就越低。

下面通过一个非常简单的程序来演示内存回收,它主要的功能是通过一个循环不断地创建一个数组,我们主要研究这些数组所占用的内存在何时会被回收。代码中的Sleep语句主要是为了让结果显示的明显一些,否则因为时间间隔太小,而看不清楚内存的变化。接下去的测试都是基于下面的代码:

第一次尝试,数组大小为1000,未开启Collect:

第一次尝试结果

上图纵坐标为程序占用的内存数(字节);横坐标是执行时间(秒)(下同)。可以看到内存一直在增加,并没有降低过。

第二次尝试,数组大小为1000,开启Collect:

第二次尝试

我们可以看到,每隔一秒钟(就是我们Sleep的时间),内存就会下降一块。

第三次尝试,数组大小为1000000,未开启Collect:

第三次尝试

结果与第二次尝试类似,因为每次创建的数组大小非常大,已经达到垃圾回收器内置的阈值,所以会自动执行回收。

垃圾回收器的工作机制比较复杂,已经不在本文的讨论范围内了,有兴趣的同学可以参考MSDN中的相关文章。推荐下面一片文章,介绍了GC的基本知识以及一些性能提升点:

http://msdn.microsoft.com/en-us/library/ms973837.aspx



Aug16