重新学.Net[六]——垃圾回收和资源管理[上]
一直觉得C++的资源管理让人很痛苦。我不得不看很多经验性手册,以保证能很好的进行内存等资源的管理。相比之下,.Net(CLR)引入了垃圾回收机制(GC),来完成托管堆资源的回收,这毫无疑问,大大减轻了开发人员的负担。但是,天下没有绝对免费的午餐,要想清楚地了解GC的运行,很好的掌握资源的管理工作(特别是涉及到非托管资源的时候),对我这种AI没有突破性进展的人来说,并不是一件很容易的事情。
恩。废话不说,来具体看看CLR的资源管理机制。首先,来看看CLR对内存(托管堆)的分配。最简单的说法就是,托管堆是被连续的一块一块分配出去的。具体一些,托管堆每次会将空堆开头的那块分配出去,然后指向新空堆的开头,方式就和栈分配类似。如果,托管堆只有这样一个分配过程的话,其速度无疑是快的可怕,那做.Net程序员也太幸福了。只可惜,内存是有限的,必须在合适的时候启动GC来进行无效对象(就是没人用了的)的回收。
GC对托管堆内存的回收是一个复杂的过程。在用一句话描述的话,就是每次启动GC,它会回收一部分无根的对象。所谓无根的对象,就是指在当前执行域中没有变量能再使用的对象。比如:
object a=new object();
a=null;
这时候原来初始化a的时候new出来的对象就成了无根对象,就是可回收的对象了。GC知道什么是无根对象吗?恩,在大部分的时候,我们要相信同志,相信它不会漏网一个无根对象也不会将一个有根的对象当无根对象干掉。再具体看看回收的算法,这会有些复杂了。简单的说GC的回收算法是建立在新对象生存期短的假设下的(这个假设是很靠谱的,考虑一下我们写的代码就可以明白,最外层的代码总是被用很久,一个for里面的变量往往用过就抛弃了)。所以它会优先回收那些比较新的对象占用的内存。更具体一些,它采用一种被称为代龄算法的回收算法,默认将所有堆中的对象分成0,1,2三个代龄,代龄越大的对象越老,越可能被继续使用,刚分配来的都为0代。每个代龄对象的总空间都有一个阈值,CLR会根据GC的执行情况动态调整这个阈值。当0代对象占用的空间超过阈值的时候,GC会启动来回收内存,先回收0代的空间,然后提升0代为1代,如果1代也满了则回收1(否则就结束了),依此类推(我也有一个疑问,2代区域满了该怎么办,是及时扩大还是先放在虚拟内存中等会扩大)。当然当然,这是一个简单的描述,算法中会考虑很多因素,可能会采取一系列的优化措施,这些细节对于大多数人来说是不需要了解的(我想了解也了解不了,呵呵)。从这个算法中我们也可看出,GC机制特别适合于大量临时对象被创建,又全部被销毁的场合,所以GC在Asp.net中的性能表现特别的突出。
上面描述的是一个宏观的过程,既考虑整体上托管堆是如何被分配和回收的。让我们再考虑具体一个对象的分配和回收。假设这个对象叫faint(^_^)。首先,faint在托管堆被分配了,这时候它为0代。很不幸,在faint还没有提升到1代的时候,它就被抛弃了,成了一个无根的对象。这时候GC老大起来收内存了,faint当然没有逃脱老大锐利的眼神。关键时刻来临了,老大会问它一个生死攸关的问题(其实不是即时判断的,而是事先用一个结构保存好了的),你的Finalize方法是不是原来的Object.Finalize()(不好意思说的那么恶心,Finalize是对象回收时被调用的一个方法,在Object类中有默认的实现,如果faint的祖先中有一个重写了Finalize方法,这个方法就不算是原有的Object.Finalize方法了),如果是,那么当场被干掉,不复存在;如果不是,faint会依然被放在堆中,在下一次GC启动的时候再被干掉(早死和晚死的问题)。故事写的很不好,需要强调的是,如果一个类在继承的结构中被重载过Finalize方法,它不会在两次GC启动后被回收。了解这个情况,在很多时候会为你顿悟埋下伏笔。
还有一个问题,就是CLR何时会启动GC?如何保证GC运行过程中,内存分配的状况不会被改变?CLR采取的策略是在合适的时机(叫安全点)劫持当前线程,启动垃圾回收线程,此时所有的其他线程被挂起,等待回收的完成。毫无疑问,这时一个性能损失大户,可喜的消息时微软会不断努力减少这种开销,至少对一般的使用不会感到这种开销的存在。另外,不只是GC会负责回收内存(也就是调用Finalize函数),当CLR卸载AppDomain或CLR关闭等时候,CLR也会遍历所有对象的Finalize函数,以便回收所有内存空间。
看了半天我们发现,这都是系统做的事情,自动分配内存,自动判断无根对象,自动启动GC,自动调用回收算法。我们有办法改变吗?答案当然是有的。你可以改变代龄阈值(没玩过,好像可以吧),最普遍的是调用GC.Collect()要求启动GC回收内存(有时候需要调用两次GC.Collect(),想一想是为什么,答案上面有哦^_^),当然很多时候是不建议这样做的,因为会带来性能损失。除非是你确实有大内存需要回收,并且这时候正好在执行一项很耗时的工作,GC的耗时可以被很好的掩盖(考虑一下,把一个长相很谦虚的mm,扔到XX影视学院和XX理工学校都会产生什么效果吧,=。=!!,呵呵,一个玩笑)。
垃圾回收不只是上面这些简单的过程。还有很多像弱类型,并发式垃圾回收之类的内容。如果有兴趣,可以好好阅读圣经《.Net框架程序设计》中对相关内容的描述,可能需要一点耐心,但绝对受益匪浅。
了解这些过程对于一般开发人员有什么用呢?至少可以谋得心里安慰,好歹知道自己new出来的东西是怎么牺牲的^_^。呵呵,当然还有其他更多的好处。比如,我现在就很明确写类似于faint=null,这样代码的意义了。这是在帮助系统明确我的对象已经无根可以回收了(不用等待跳出有效域了)。还有我也不会乱写GC.Collect()来戏弄CLR了。另外,在后面会说其他非托管资源的管理工作,也许理解这些会有所帮助的。
PS:写那么多体力告罄了,只能分个上下,关于非托管资源的回收明天再写好了。。。
posted on 2007-02-09 16:46 duguguiyu 阅读(1106) 评论(3) 编辑 收藏 举报