垃圾回收器(GC)为你控制托管内存。不像本地运行环境,你不用负责对内存泄漏,不定指针,未初始化指针,或者一个其它内存管理的服务问题。但垃圾回收器前不是一个神话:你一样要自己清理。你要对非托管资源负责,例如文件句柄,数据链接,GDI+对象,COM对象,以及其它一些系统对象。
这有一个好消息:因为GC管理内存,明确的设计风格可以更容易的实现。循环引用,不管是简单关系还是复杂的网页对象,都非常容易。GC的标记以及严谨的高效算法可以检测到这些关系,并且完全的删除不可达的网页对象。GC是通过对从应用程序的根对象开始,通过树形结构的“漫游”来断定一个对象是否可达的,而不是强迫每个对象都保持一些引用跟踪,COM就是这样的。DataSet就是一个很好的例子,展示了这样的算法是如何简化并决定对象的所属关系的。DataSet是一个DataTable的集合,而每一个DataTable又是DataRow的集合,每一个DataRow又是DataItem的集合,DataColum定义了这些类型的关系。这里就有一些从DataItem到它的列的引用。而同时,DataTime也同样有一个引用到它的容器上,也就是DataRow。DataRow包含引用到DataTable,最后每个对象都包含一个引用到DataSet。
(译注:作者这里是想说:你看,这么复杂的引用关系,GC都可以轻松的搞定,你看GC是不是很强大?)
如果这还不够复杂,那可以创建一个DataView,它提供对经过过滤后的数据表的顺序访问。这些都是由DataViewManager管理的。所有这些贯穿网页的引用构成了DataSet。释放内存是GC的责任。因为.Net框架的设计者让你不必释放这些对象,这些复杂的网页对象引用不会造成问题。没有必须关心这些网页对象的合适的释放顺序,这是GC的工作。GC的设计结构可以简化这些问题,它可以识别这些网页对象就是垃圾。在应用程序结束了对DataSet的引用后,没有人可以引用到它的子对象了(译注:就是DataSet里的对象再也引用不到了)。因此,网页里还有没有对象循环引用DataSet,DataTables已经一点也不重要了,因为这些对象在应用程序都已经不能被访问到了,它们是垃圾了。
垃圾回收器在它独立的线程上运行,用来从你的程序里移除不使用的内存。而且在每次运行时,它还会压缩托管堆。压缩堆就是把托管堆中活动的对象移到一起,这样就可以空出连续的内存。图2.1展示了两个没有进行垃圾回收时的内存快照。所有的空闲内存会在垃圾回收进行后连续起来。
图2.1 垃圾回收器不仅仅是移动不使用的内存,还移除动其它的对象,从而压缩使用的内存,让出最多的空闲内存。
正如你刚开始了解的,垃圾回收器的全部责任就是内存管理。但,所有的系统资源都是你自己负责的。你可以通过给自己的类型定义一个析构函数,来保证释放一些系统资源。析构函数是在垃圾回收器把对象从内存移除前,由系统调用的。你可以,也必须这样来释放任何你所占用的非托管资源。对象的析构函数有时是在对象成为垃圾之后调用的,但是在内存归还之前。这个非确定的析构函数意味着在你无法控制对象析构与停止使用之间的关系(译注:对象的析构与对象的无法引用是两个完全不同的概念。关于GC,本人推荐读者参考一下Jeffrey的".Net框架程序设计(修订版)"中讨论的垃圾回收器)。对C++来说这是个重大的改变,并且这在设计上有一个重大的分歧。有经验的C++程序员写的类总在构造函数内申请内存并且在析构函数中释放它们:
// Good C++, bad C#:
class CriticalSection
{
public:
// Constructor acquires the system resource.
CriticalSection( )
{
EnterCriticalSection( );
}
// Destructor releases system resource.
~CriticalSection( )
{
ExitCriticalSection( );
}
};
// usage:
void Func( )
{
// The lifetime of s controls access to
// the system resource.
CriticalSection s;
// Do work.
//...
// compiler generates call to destructor.
// code exits critical section.
}
这是一种很常见的C++风格,它保证资源无异常的释放。但这在C#里不工作,至少,与这不同。明确的析构函数不是.Net环境或者C#的一部份。强行用C++的风格在C#里使用析构函数不会让它正常的工作。在C#里,析构函数确实是正确的运行了,但它不是即时运行的。在前面那个例子里,代码最终在critical section上,但在C#里,当析构函数存在时,它并不是在critical section上。它会在后面的某个未知时间上运行。你不知道是什么时候,你也无法知道是什么时候。
依懒于析构函数同样会导致性能上的损失。须要析构的对象在垃圾回收器上放置了一剂性能毒药。当GC发现某个对象是垃圾但是须要析构时,它还不能直接从内存上删除这个对象。首先,它要调用析构函数,但析构函数的调用不是在垃圾回收器的同一个线程上运行的。取而代之的是,GC不得不把对象放置到析构队列中,让另一个线程让执行所有的析构函数。GC继续它自己的工作,从内存上移除其它的垃圾。在下一个GC回收时,那些被析构了的对象才会再从内存上移除。图2.2展示了三个内存使用不同的GC情况。注意,那些须要析构的对象会待在内存里,直到下一次GC回收。
图2.2 这个顺序展示了析构函数在垃圾回收器上起的作用。对象会在内存里存在的时间更长,须要启动另一个线程来运行垃圾回收器。
这用使你相信:那些须要析构的对象在内存至少多生存一个GC回收循环。但,我是简化了这些事。实际上,因为另一个GC的介入(译注:其实只有一个GC,作者是想引用回收代的问题。),使得情况比这复杂得多。.Net回收器采用”代“来优化这个问题。代可以帮助GC来很快的标识那些看上去看是垃圾的对象。所以从上一次回后开始创建的对象称为第0代对象,所有那些经过一次GC回收后还存在的对象称为第1代对象。所有那些经过2次或者2次以上GC回收后还存在的对象称为第2代对象(译注:因为目前GC只支持3代对象,第0代到第2代,所以最多只有第2代对象,如果今后GC支持更多的代,那么会出现更代的对象,.Net 1.1与2.0都只支持3代,这是MS证实比较合理的数字)。
分代的目的就是用来区分临时变量以及一些应用程序的全局变量。第0代对象很可能是临时的变量。成员变量,以及一些全局变量很快会成为第1代对象,最终成为第2代对象。
GC通过限制检测第1以及第2代对象来优化它的工作。每个GC循环都检测第0代对象。粗略假设个GC会超过10次检测来检测第0代对象,而要超过100次来检测所有对象。再次考虑析构函数的开销:一个须要析构函数的对象可能要比一个不用析构函数的对象在内存里多待上9个GC回收循环。如果它还没有被析构,它将会移到第2代对象。在第2代对象中,一个可以生存上100个GC循环直到下一个第2代集合(译注:没理解,不知道说的什么)。
结束时,记得一个垃圾回收器负责内存管理的托管环境的最大好处:内存泄漏,其它指针的服务问题不在是你的问题。非内存资源迫使你要使用析构函数来确保清理非内存资源。析构函数会对你的应用程序性能产生一些影响,但你必须使用它们来防止资源泄漏(译注:请注意理解非内存资源是什么,一般是指文件句柄,网络资源,或者其它不能在内存中存放的资源)。通过实现IDisposable接口来避免析构函数在垃圾回收器上造成的性能损失。接下来的具体的原则将会帮助你更有效的使用环境来开发程序。