三文鱼海域

Fools learn nothing from wise men, but wise men learn much from fools.

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  33 随笔 :: 3 文章 :: 113 评论 :: 0 Trackbacks
  作为一位C++出身的C#程序员,我最初对垃圾收集(GC)抱有怀疑态度,怀疑它是否能够稳定高效的运作;而到了现在,我自己不得不说我已经逐渐习惯并依赖GC与我的程序“共同奔跑”了,对“delete”这个习惯于充当罪魁祸首的关键字也渐渐产生了陌生感。然而实践证明,我对GC的过分信赖却招致了很多意想不到的错误,这也激励了我对GC的运作机制作深入一步的了解。随后我开始翻书,查资料,终于对GC有了一个比较完整的理解(但远远算不上深入)。有人也许会说:“研究GC的内部机制有什么价值吗?我们是搞应用程序开发的,客户的机器可以达到很高的配置,内存资源不是问题。”这种说法明显是认为“垃圾收集=内存释放”了,其实在垃圾收集中,造成最多麻烦的往往不是内存量,而是在内存释放之外,GC暗地里为我们做的繁杂事务(例如非托管资源的清理和释放)。如果你对GC的基本运作还不了解,而又没有时间仔细阅读众多技术资料的话,那么我的这几篇文章或许对你能有一些帮助。

下面就从资源的分配和释放入手,先了解一下背景知识。

 

. 托管资源的分配

 CLR在运行时管理着一段内存地址空间(虚拟地址空间,在运行中会映射到物理内存地址中),分为“托管堆”和“栈”两部分,栈用于存储值类型数据,它会在方法执行结束后自动销毁其中引用的值类型变量,这一部分不属于垃圾收集的范围。托管堆用于引用类型的变量存储,是垃圾收集的关键阵地。

托管堆是一段连续的地址空间,其中所分配出去的空间呈现出类似数组形态的队列结构: 

 NextObjPtr是托管堆所维护的一个内存指针,指示下一个对象分配的内存起始地址,它会随着内存的分配而不断移动(当然也会随着内存垃圾回收而发生移动),永远指向下一个空闲的地址。 

到了这里,我们不妨与C++比较一下内存分配机制的效率(对效率不感兴趣的大可以跳过:)),顺便让C++的朋友们打消一些对CLR分配内存效率的疑虑。在查找空闲内存空间时,CLR只需要在NextObjPtr处直接留出指定大小的空间提供给数据初始化,然后计算新的空闲地址并重置NextObjPtr指针即可。而在C/C++中,在分配内存之前先要遍历一遍内存占用的链表以查找合适大小的内存块,然后再修改此链表,这样也很容易产生内存碎块,使得内存分配性能下降。很明显,.NET的分配方式效率更高。但是这种效率是以GC的劳动为代价的。 

. 垃圾判定

    要进行垃圾收集,首先要知道什么是垃圾。GC通过遍历应用程序中的“根”来寻找垃圾。我们可以认为根是一个指向引用类型对象内存地址的指针。如果一个对象没有了根,就是它不再被任何位置所引用,那么它就是垃圾的候选者了。

1 public static void Main()
2             {
3                 string sGarbage = "I'm here";
4 
5                 //下面的代码没有再引用s,它已经成为垃圾对象---当然,这样的代码本身也是垃圾;
6                 //此时如果执行垃圾收集,则sGarbage可能已经魂归西天
7 
8                 Console.WriteLine("Main() is end");
9             }

    值得注意的一点是,对象可能在其生存期结束之前就被列入垃圾名单,甚至已经被GC所暗杀!那是因为对象可能在生存期的某一时刻已经不再被引用,如果在这个时候执行垃圾收集,那么这个不幸的对象极有可能已经被列为垃圾并被销毁(为什么说是“可能”呢?因为它不一定在GC的视力范围内。后面讲到“代龄”时会详细介绍相关细节)。 

. 对象代龄

    尽管GC总是在默默为我们劳动,但它毕竟是由人创造的,人会偷懒,它也会。为了减少每次的工作量,它总是希望能够减少工作的范围;它坚信,越晚创建的对象往往越短命,因此它会集中精力处理这一部分的内存区域,暂且搁置其他部分。GC引入“代龄”的概念来划分对象生存级别。

    CLR初始化后的第一批被创建的对象被列为0代对象。CLR会为0代对象设定一个容量限制,当创建的对象大小超过这个设定的容量上限时,GC就会开始工作,工作的范围是0代对象所处的内存区域,然后开始搜寻垃圾对象,并释放内存。当GC工作结束后,幸存的对象将被列为第1代对象而保留在第1代对象的区域内。此后新创建的对象将被列为新的一批0代对象,直到0代的内存区域再次被填满,然后会针对0代对象区域进行新一轮的垃圾收集,之后这些0代对象又会列为第1代对象,并入第1代区域内。第1代区域起初也会被设上一个容量限制值,等到第1代对象大小超过了这个限制之后,GC就会扩大战场,对第1代区域也做一次垃圾收集,之后,又一次幸存下来的对象将会提升一个代龄,成为第2代对象。 

     可见,有一些对象虽然符合垃圾的所有条件,但它们如果是第1代(甚至是第2代老臣)对象,并且第1代的分配量还小于被设定的限制值时,这些垃圾对象就不会被GC发现,并且可以继续存活下去。

    另外,GC还会在工作过程中汲取经验,根据应用程序的特点而自动调整每代对象区域的容量,从而可以更高效的工作。

posted on 2006-08-27 23:15 三文鱼 阅读(1431) 评论(10)  编辑 收藏

评论

#1楼  2006-08-28 09:03 xc#      
兄弟,图片出不来啊
  回复  引用  查看    

#2楼  2006-08-28 10:20 Pharaoh      
都是本机的图片。
  回复  引用  查看    

#3楼  2006-08-28 10:23 糊涂小猪      
而在C/C++中,在分配内存之前先要遍历一遍内存占用的链表以查找合适大小的内存块..
在C/C++ 分配内存空间好像也是有三种的分配分式。其中有一种就是像直接留出指定大小的空间提供给数据初始化,然后计算新的空闲地址并重置NextObjPtr指针即可。

所以如果是一同样的方式来分配的话,效率是否会高?
  回复  引用  查看    

#4楼  2006-08-28 11:02 drdirac [未注册用户]
现在的C/C++中的内存分配器不一定是要遍历内存占用链表了。
遍历本身就用很多策略,比如最优适配,最小适配等。
通常会维护一个树结构来加速内存块的查找,比如glibc里的malloc就是这样的。
而且一般的内存分配器(像上面说的malloc)会对小块内存的分配做优化。如果你分配几十个字节乃至上百个字节的内存块(具体什么叫小内存要看内存管理器的设置),只要从一个自由链表里移出开头一块,速度是非常快的。
比较大的问题除了速度还有碎片。毕竟malloc和new这样的内存管理器是所谓的通用内存管理器,要考虑到一切可能的需要。对特殊的场合,比如始终分配大小一样的内存块,可以定制内存管理器来优化效率,采用内存池这样的技术。SGI STL的allocator就是特别优化过的,性能很好。
  回复  引用    

#5楼 [楼主] 2006-08-28 11:28 三文鱼      
图片现在应该可以看到了:)。

谢谢drdirac的补充,你知道的C++细节比我多。
据我所知,操作系统会维护一个空闲的内存链表,而malloc等函数本身也会维护一个内部的堆结构,在分配内存块时,malloc会先从内部堆中查找,如果没有符合要求的空闲内存时,再调用操作系统函数(内部会遍历空闲内存链表)以申请额外的内存空间。这是一个大概的流程,当然在函数库内部还会对内部堆结构进行优化,不同的库也有不同的实现方式。但是无论如何,毕竟免不了经过一个查找的过程;而C#的分配方式可以省去这一步,其速度几近于栈空间的分配速度,理论上应该会更高一些。不知道我的理解对不对。
糊涂小猪 说的应该是栈的分配方式吧。

  回复  引用  查看    

#6楼  2006-08-30 08:23 drdirac [未注册用户]
我不是很清楚.NET所采用的垃圾回收算法。最快的算法无疑是直接从连续的内存块上取,就是你所说的方法。但是由于不断的分配和回收,内存块一定会出现很多碎片,也就是说不连续了,所以要进行查找。垃圾回收算法里面有一种复制机制,就是在整理时把所有对象复制到一个连续的内存块里面,同时调整所有相关的引用。否则不能保持堆的连续性。这样能保证分配时的高效率,但需要在回收时多耗费一些时间。
  回复  引用    

#7楼  2006-08-30 10:24 小生      
@drdirac
在整理时把所有对象复制到一个连续的内存块里面
-------------------

it will spend long time if there is a big object.

A book named "Applied Microsoft.Net Framework Programming"(Chinese name:.NET框架程序設計(修訂版))
has a chapter That focus the .net garbage collecting and explains it detailedly.
  回复  引用  查看    

#8楼  2006-08-30 11:52 drdirac [未注册用户]
@小生
这本书也算是.NET的经典了
作者Jeffrey Richter专门写过一篇长文来解释.NET中的GC机制,可以看这里
http://msdn.microsoft.com/msdnmag/issues/1100/gci/
  回复  引用    

#9楼  2006-08-30 14:35 Colin Han      
学到不少东西,关注一下。
期待下文。
  回复  引用  查看    

#10楼  2007-06-23 08:20 孤剑      
这几天就在关注这个东西了学习一下。
  回复  引用  查看    


标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2006-08-29 08:57 编辑过


相关链接: