在开发.NET程序过程当中,因为CLR中的垃圾回收(garbage collection)机制会管理已分配的对象,因此程序员就能够不用关注对象何时释放内存空间了。可是,了解垃圾回收机制仍是颇有必要的,下面咱们就看看.NET垃圾回收机制的相关内容。程序员
在C#中,咱们能够经过new关键字建立一个引用类型的对象,好比下面一条语句。New关键字建立了一个Student类型的对象,这个新建的对象会被存放在托管堆中,而这个对象的引用会存放在调用栈中。(对于引用类型能够查看,C#中值类型和引用类型)编程
Student s1 = new Student();
在C#中,当上面的Student对象被建立后,程序员就能够不用关心这个对象何时被销毁了,垃圾回收器将会在该对象再也不须要时将其销毁。安全
当一个进程初始化后,CLR就保留一块连续的内存空间,这段连续的内存空间就是咱们说的托管堆。.NET垃圾回收器会管理并清理托管堆,它会在必要的时候压缩空的内存块来实现优化,为了辅助垃圾回收器的这一行为,托管堆保存着一个指针,这个指针准确地只是下一个对象将被分配的位置,被称为下一个对象的指针(NextObjPtr)。为了下面介绍垃圾回收机制,咱们先详细看看new关键字都作了什么。app
当C#编译器遇到new关键字时,它会在方法的实现中加入一条CIL newobj命令,下面是经过ILSpy看到的IL代码。函数
IL_0001: newobj instance void GCTest.Student::.ctor()
其实,newobj指令就是告诉CLR去执行下列操做:性能
按照上面的分析,当咱们建立两个Student对象的时候,托管堆就应该跟下图一致,NextObjPtr指向托管堆新的可用地址。优化
托管堆的大小不是无限制的,若是咱们一直使用new关键字来建立新的对象,托管堆就可能被耗尽,这时托管堆能够检测到NextObjPtr指向的空间超过了托管堆的地址空间,就须要作一次垃圾回收了,垃圾回收器会从托管堆中删除不可访问的对象spa
垃圾回收器是如何肯定一个对象再也不须要,能够被安全的销毁?线程
这里就要看一个应用程序根(application root)的概念。根(root)就是一个存储位置其中保存着对托管堆上一个对象的引用,根能够属性下面任何一个类别:指针
垃圾回收能够分为两个步骤:
下面结合应用程序的根的概念,咱们来看看垃圾回收这两个步骤。
在垃圾回收的过程当中,垃圾回收器会认为托管堆中的全部对象都是垃圾,而后垃圾回收器会检查全部的根。为此,CLR会创建一个对象图,表明托管堆上全部可达对象。
假设托管堆中有A-G七个对象,垃圾回收过程当中垃圾回收器会检查全部的对象是否有活动根。这个例子的垃圾回收过程能够描述以下(灰色表示不可达对象):
代码中颇有可能多个对象中引用了同一个对象E,垃圾回收器只要检测到对象E已经被标记过,则再也不对对象E内所引用的对象进行检测,这样作有两个目的:一是提升性能,二是避免无限循环。
全部的根对象都检查完以后,有标记的对象就是可达对象,未标记的对象就是不可达对象。
继续上面的例子,垃圾回收器将销毁全部未被标记的对象,释放这些垃圾对象所占的内存,再把可达对象移动到这里以压缩堆。
注意,在移动可达对象以后,全部引用这些对象的变量将无效,接着垃圾回收器要从新遍历应用程序的全部根来修改它们的引用。在这个过程当中若是各个线程正在执行,极可能致使变量引用到无效的对象地址,因此整个进程的正在执行托管代码的线程是被挂起的。
通过了垃圾回收以后,全部的非垃圾对象被移动到一块儿,而且全部的非垃圾对象的指针也被修改为移动后的内存地址,NextObjPtr指向最后一个非垃圾对象的后面。
当CLR试图寻找不可达对象的时候,它须要遍历托管堆上的对象。随着程序的持续运行,托管堆可能愈来愈大,若是要对整个托管堆进行垃圾回收,势必会严重影响性能。因此,为了优化这个过程,CLR中使用了"代"的概念,托管堆上的每个对象都被指定属于某个"代"(generation)。
"代"这个概念的基本思想就是,一个对象在托管堆上存在的时间越长,那么它就更可能应该保留。托管堆中的对象能够被分为0、一、2三个代:
下面仍是经过一个例子看看代这个概念(灰色表明不可达对象):
经过前面的描述能够看到,分代能够避免每次垃圾回收都遍历整个托管堆,这样能够提升垃圾回收的性能。
.NET类库中提供了System.GC类型,经过该类型的一些静态方法,能够经过编程的方式与垃圾回收器进行交互。
看一个简单的例子:
class Student { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public string Gender { get; set; } } class Program { static void Main(string[] args) { Console.WriteLine("Estimated bytes on heap: {0}", GC.GetTotalMemory(false)); Console.WriteLine("This OS has {0} object generations", GC.MaxGeneration); Student s = new Student { Id = 1, Name = "Will", Age = 28, Gender = "Male"}; Console.WriteLine(s.ToString()); Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s)); GC.Collect(); Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s)); GC.Collect(); Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s)); Console.Read(); } }
程序的输出为:
从这个输出,咱们也能够验证代的概念,每次垃圾清理后,若是一个对象没有被清理,那么它的代就会提升。
因为托管堆上的对象由垃圾管理器帮咱们管理,全部咱们不须要关心托管堆上对象的销毁以及内存空间的回收。
可是,有些特殊的状况下,咱们可能须要经过GC.Collect()强制垃圾回收:
在使用强制垃圾回收时,建议同时调用"GC.WaitForPendingFinalizers();",这样能够肯定在程序继续执行以前,全部的可终结对象都必须执行必要的清除工做。可是要注意,GC.WaitForPendingFinalizers()会在回收过程当中挂起调用的线程。
static void Main(string[] args) { …… GC.Collect(); GC.WaitForPendingFinalizers(); …… }
每一次垃圾回收过程都会损耗性能,因此要尽可能避免经过GC.Collect()进行强制垃圾回收,除非遇到了真的须要强制垃圾回收的状况。
本文介绍了.NET垃圾回收机制的基本工做过程,垃圾回收器经过遍历托管堆上的对象进行标记,而后清除全部的不可达对象;在托管堆上的对象都被设置了一个代,经过了代这个概念,垃圾回收的性能获得了优化。
下一篇咱们看看可终结对象(Finalize)和可处置对象(IDisposable)。