最近面试了一些人,发现对.NET的GC(垃圾回收)的理解都存在错误。GC其实是相当复杂的系统,虽然95%的情况下我们并不需要考虑它,但仍有5%的情况我们不得不接触GC体系来解决问题。比如这个问题:
void Func()
{
A a = new A();
B b = new B();
a.RefToB = b;
b.RefToA = a;
}
那么a和b会不会被GC回收?好几个人都答错。如果你按照COM的模式去思考GC,那就完全错误了。每次我问“什么情况下,对象会被GC回收?”,他们都能回答上来“当程序里没有对对象的引用时”。但是错了,为什么?如果你还没明白,就再看看上面这段代码。
GC管理对象不是用的COM的引用计数模式。事实上最初微软确实想用引用计数方式实现GC,这样的一个优势就是对象的析构时机是确定的,当引用计数为0时,对象会被析构,之后也不会再有任何代码能够再访问该对象,这是很理想的情况。但经过反复实验,这种方法被抛弃了。一个原因就是如上的例子,会导致对象无法释放。另一个重要原因就是应用计数的额外开销对高性能程序不可接受。尤其是在多线程情况下,因为.NET使用自由线程模型,多个线程可能同时访问一个对象,每一个引用计数的增减操作都不得不做线程同步。
.NET采用的GC模式是分代GC(Generational GC),堆空间按对象的生存期长短分成3代。新分配的对象在第0代,按地址顺序分配,当第0代的空间(约几百K)用光时,将程序里能引用到的对象移动到第1代,那么剩下的就是垃圾,第0代空间便可以重新用于分配。同理,第1代也按同样的逻辑运行,那么第2代里的对象将都是生存期很长的对象。由此可以推出如下几点:
1)对象的分配时间开销远小于C++的堆分配,但回收时间开销大于分配时间开销。
2)不会出现C++里的堆碎片过多的问题,有利于程序长时间运行。
3)循环引用的对象能够被正确回收。
那么再回答开始的问题,“什么情况下,对象会被GC回收?”正确答案是“当程序里没有对对象的活引用(Alive reference)时。”
当然,实际情况比如上所述复杂得多,有兴趣可以考虑如下问题:
1)Finalizer在哪里、什么时候执行?如果抛了异常会有什么后果?
2)大于第0代空间的大数组是怎么分配的?
3)为什么要求Dispose()可以被调用多次?
.NET的GC理解误区
最新推荐文章于 2025-05-08 22:13:21 发布
.NET的GC理解误区