微软声称.NET是一种革命性的编程技术。许多要素使它成为大多数开发人员的首选。本文我们将要讨论一下.NET Framework中一个很主要的优势——内存和资源管理的便捷性。
关于垃圾回收器
每个程序都会使用一定次序的资源,或内存缓冲区,或网络连接,或数据库资源等等。实际上,在面向对象环境中,每种类型都被看作是程序的某些有效资源。为了使用这些资源,必须分配部分内存来描述这种类型。
资源访问按一下步骤进行:
1. 为类型分配内存来描述资源。
2. 初始化资源,把资源设置成初始化状态,使资源可用。
3. 通过访问类型实例的成员来使用资源(按需求重复)。
4. 销毁资源状态以清除资源。
5. 释放内存。
.NET中的垃圾回收器(GC)完全彻底帮助开发人员从追踪内存的使用和确定什么时候释放内中解脱出来。
Microsoft® .NET CLR (公共语言运行库)要求所有资源都从托管堆中进行分配。你无需释放托管堆中的对象——当应用程序不再需要这些对象时,对象将被自动释放。
内存不是无限的。垃圾回收器需要执行回收以便释放内存。垃圾回收器的优化引擎会对已做的分配选择最好的回收时间(准确标准由微软提供)。当垃圾回收器执行回收时,它先找出托管堆中不再被应用程序使用的对象然后执行相应操作收回这些对象的内存空间。
然而,为了进行自动内存管理,GC必须知道根的位子。也就是说,它应该知道一个对象什么时候不再被应用程序使用了。在.NET中,GC是通过一个称之为元数据的东西了解到这些的。.NET中使用的每种数据类型都通过元数据来描述它。在元数据的帮助下,CLR知道内存中每个对象的布局,在垃圾回收的整理阶段给GC提供帮助。没有这些信息,GC将不会知道一个对象在哪儿结束和下一个从哪儿开始。
垃圾回收算法
应用程序根(Application Roots)
每个应用程序都有一套根(Roots)。根标识存储位置,这个位置或者指向一个托管堆的对象,或者指向一个空对象(null)。
比如:
l 一个应用程序中所有全局和静态对象指针。
l 一个线程堆栈中所有局部变量/参数对象指针。
l 托管堆中所有CPU登记的对象指针。
l FReachable队列中的对象指针。
活动根的表由JIT编译器和CLR维护,并且对于垃圾回收器的算法是访问。
实现
.NET中的垃圾回收是用跟踪回收实现的,确切的说CLR实现了标记(Mark)/整理(Copact)回收器。
这个方法有以下两个阶段组成:
阶段I:标记(Mark)
找到可以被收回的内存。
当GC开始运行时,它假设堆中的所有对象都是垃圾。换句话说,它假设应用程序的根没有指向堆中的的任何对象。
阶段I中包含下列步骤:
1. GC识别存活对象的引用或应用程序根。
2. 从根开始遍历,建一张可以从根遍历的所有对象的图。
3. 如果GC准备尝试添加一个已经在图中的对象,它就停止这条路径的遍历。这样做有两个目的,第一个是极大的优化性能,因为它不会遍历一套对象一次以上。第二是防止当有对象的循环连接列表时而发生死循环,因此循环被有效的控制了。
一旦所有的根都被检查完后,垃圾回收器的图中包含了所有可以从应用程序根遍历到的对象。任何不再图中的对象都不能被应用程序访问到,也就是所谓的垃圾。
阶段II:整理(Compact)
把所有存活的对象移到堆的末端,空出堆顶端的空间。
阶段II包含下列步骤:
1. 现在GC线性地遍历堆,寻找邻近的垃圾对象块(现在被认为是空闲空间)。
2. 然后GC往下移动内存中的非垃圾对象,去掉堆中的所有空隙。
3. 移动内存中的对象导致对象指针失效。因此GC需要修改应用程序的根使对象的指针指向新的位置。
4. 另外,如果对象包含一个指向其它对象的指针,GC也会负责纠正这些指针。
在所有垃圾被标识完以后,所有的非垃圾对象也被整理,并且所有非垃圾对象的指针也被修正,最后一个非垃圾对象后的指针指向下一个被添加对象的位置。
终结(Finalization)
.NET Framework的垃圾回收器能暗中追踪由应用程序创建的对象的生命周期,但是当它遇到对象包装了非托管资源(比如文件、窗口或网络连接等)时却无能为力。
一旦应用程序不再使用那些非托管资源时需要显示地释放它们。.NET Framework为对象提供了终结(Finalize)方法:在垃圾回收器收回这个对象的内存时,必须执行对象的这个方法来清除它的非托管资源。由于缺省的Finalize方法什么都没做,如果需要显示清除资源必须覆盖这个方法。
如果一把Finalize方法当作只是C++中析构函数另外一个名字那也不足为怪。虽然它们都被赋予了释放对象占有的资源的任务,但是它们还是有很不相同的语义。在C++中,当对象推出作用域时析构函数会立刻被调用,而Finalize方法是在起动垃圾回收清除对象时才被调用的。
.NET中,由于终结器(Finalizer)的存在使得垃圾回收的工作变得更加复杂了,因为它在释放对象前增加了许多额外的操作。
无论什么时候,在堆上分配一个含有Finalize方法的新对象时,都会有一个指向这个对象的指针被添加到一个称为Finalization队列的内部数据结构当中。当对象不能再次被遍历到时,GC就认为这个对象是垃圾。GC首先扫描Finalization队列查找这些对象的指针,当指针被找到时,把它从Finalization队列中去掉并添加到另外一个名为FReachable队列的内部数据结构中,使这个对象不再是垃圾的一部分。这时,GC完成了确定垃圾。然后整理(Compact)可收回的内存,由专门的线程负责清空FReachable队列并执行对象的Finalize方法。
第二次垃圾回收器被触发的时候,它把被终结(Finalize)的对象看作真正的垃圾,然后简单的释放它们的内存。
由此可知当一个对象需要终结时,它先死,然后存活(复活),然后再次并且最终地死去。推荐避免使用Finalize方法,除非有需要。Finalize方法会增加内存压力,因为直到两次垃圾回收被启动时,对象占用的内存和资源才会得到释放。因此你无法控制两次Finalize方法执行的顺序,它可能会导致无法预料的后果。
垃圾回收性能的优化
l 弱引用(WeakReference)
l 代(Generations)