我们平常写程序很少自己去写资源管理的,除非写非常大型的应用程序,或者大公司自己的sdk。看到过PGP源代码的一定知道,PGP的SDK就实现了自己的内存管理。自己管理内存烦恼实在多多,忘记释放了,释放了又再次访问的bug层出不穷,这种bug又非常难查。普通的逻辑bug,简单测试发现程序没有按照预想的运行就可以找到。但是内存的问题却很难发现。过去很多公司也为解决这方面的问题作过很大努力,比如Compuware的BoundsChecker,Rational的Purify。这些工具使用起来也很困难,经常找到的点都是你用的开发库的代码。现在.NET提供了全套的资源管理(无用资源回收:Garbage Collection,简称GC),能够让我们从中解脱出来,把精力用在自己应该解决的业务问题上去。当然它不是万能的,我们最好多了解他的原理,以便我们可以更好的享用它。
系统资源不是无限的,内存用完要释放,文件,网络连接都是系统资源,用完之后也要释放。在面向对象的系统中,所有的东西都是对象,所以使用任何资源都要从系统分配内存,最后释放它。使用资源的过程无外乎五个步骤:
1. 为代表资源的类型分配内存;
2. 初始化资源状态,请求非内存系统资源(如打开文件,建立网络连接等);
3. 通过访问类型的实例(对象)及其成员变量、方法等来访问资源;(可能多次)
4. 清空资源状态,释放系统资源(如关闭文件,关闭网络连接等);
5. 释放内存
我们遇到的内存问题一般都在上面的五个步骤中,.NET提供的无用单元回收(GC)机制基本上都可以解决这些问题。不过GC是不知道如何清空资源状态和释放系统资源的(即上面的第四步),这就要利用到Finalize方法,这个我们后面讨论。当然,大部分对象,如字符串等是无需这第四步的。
CLR实现了一个托管堆(Managed Heap),要求所有的资源都必须从这个堆中分配,并且无需释放。下面就详细说明对象是如何在堆中分配,GC又是如何做无用单元回收的。
CLR在进程初始化时,保留一块连续的内存,这块连续的内存就是托管堆。CLR同时为托管堆维护一个指针,这个指针永远指向下一个可以分配的内存空间,我们这里叫NextObjPtr。
当程序适用new创建一个对象时,new首先确认堆中是否有足够的内存空间,如果有的话,则为对象分配空间,调用对象的构造函数,返回分配空间的地址,接着NextObjPtr指向剩余空间的地址,即下一个可以分配的地址。如下图:
图中虚线是NextObjPtr的起始地址,分配对象C成功并返回地址后,NextObjPtr移到实线的位置。
再让我们看看普通应用程序的堆内存分配方式。普通的内存分配方式维护一个空闲内存列表,系统首先遍历空闲空间列表,找到一个足够大的空间,然后将其拆分出足够的空间分配,然后再将剩下的空间加入到空闲空间列表。在历史上有很多的实现进程堆内存分配的算法,比如比较著名的二分法等等。但是比较来看,.NET的内存分配方法要快的多。
不过内存不是无限的,堆的空间分配光了怎么办?CLR在分配内存的时候,如果发现堆中的空闲空间不足时,就会启动无用空间回收。GC将堆中不再被使用的对象占用的内存释放掉,然后将堆整理,使其剩下连续的空间以待分配,当然如果没有可以释放的对象或者释放后内存还是不够的话,就抛出OutOfMemoryException异常。GC是如何断定一个对象不再被使用了呢?
每一个应用都有一组根,这些根包括了标示托管堆中一组对象的存储单元。被认为是根的对象包括:
1. 所有的全局和静态对象;
2. 一个线成栈中的局部对象和函数的参数;
3. 任何CPU寄存器包含的指针指向的对象;
上面根的列表由JIT和CLR维护并且GC可以访问。
开始无用单元回收后GC就开始遍历根,找到第一个根指向的对象,沿着这个对象向下找,找到这个对象所有引用的对象,以及引用之引用,并将其放入到一个集合中。这一个完成后,就接着找下一个根。一旦GC发现某个对象已经在集合中,就停止这个分支的搜寻以防止重复和引用死循环。
完成后,GC就有了一个所有根可以访问到的对象的集合,这个集合中没有的对象就认为是无用的。如下图:
GC的集合包括了对象A、B、D、F,而对象C、E、G就是无用对象,GC将释放其资源。GC接着遍历堆中的所有对象,释放无用对象,并将有用对象向内存的地位移动(据说使用的memcpy),以保证空闲空间的连续性。NextObjPtr就被指向空闲空间的开始地址。这样做会使一些对象的引用地址失效,GC负责更正这些指针。回收后的堆如下图:
这一次回收所作的工作不可谓不复杂,消耗的CPU时间也是很多。不过还好,它不是时时刻刻都在运行,而是只在堆满了之后才回收(实际上是Generation 0满了之后,Generation我将在接下来的文章讨论),其他分配内存的时候还是非常快的。而且.NET提供丰富的设置来提高无用单元回收的效率。
我将在下一节讨论Finalization和Generation,强引用,弱引用。