高效使用GC - 第一部分

来源:互联网 发布:怎么在mac上卸载程序 编辑:程序博客网 时间:2024/06/06 00:43

原文出自:http://blogs.msdn.com/maoni/archive/2004/06/15/156626.aspx

 

版权归原文作者所有,转载请注明出处


高效使用GC-第一部分

 

本文的目的是讨论GC操作的成本,以便在使用托管内存时能作出正确决策- 注意不是讨论GC本身-而是如何使用GC。本文假定您对使用GC,而不是实现GC更有兴趣。同时假定对GC有基本的了解 。如果需要了解一些背景知识的话,推荐阅读Jeff Richter 发表在MSDN上的两篇文章: 1  2.

首先我们关注Workstation GC,然后讨论其与server GC的不同之处(有时候你不需要选择使用哪个,稍后我讲解释为什么)

 

拥有三个代的原因是:希望在优化过的应用程序中,大部分对象仅存活在0代。举个例子,在一个服务器应用程序中,为每一个请求分配的内存应该在请求结束后被释放。接着请求相关的对象被提升到1代然后死亡。本质上讲,1代充当了年轻对象和年长对象之间的缓冲区。当在性能监视器查看搜集次数时,2代应当比0代搜集频率低。1代堆的搜集次数相对不那么重要。并且搜集1代比0代的成本低。

 

GC

 

首先看一下GC如何从操作系统获得内存。GC在段内保留内存,每个段16MB。当EE (执行引擎)开始,会保留一些初始段 -  一个用作小对象堆,另外一个用作大对象堆。

在需要时,内存被提交或者撤销提交。用完这些段之后将保留一个新段。每次完全搜集时,不被使用的段将被删除。

大对象对总是在自己段上 - 大对象被区别于小对象来对待,它们之间不共享段。

 

分配

GC堆上分配时,实际的成本是多少?如果我们不需要进行垃圾搜集,那么分配操作将1)向前移动指针 并且2)为新对象清理内存。对于可终结的对象,还有一个额外的步骤:将其加入到一个被GC监视的列表。

注意,我说的是“如果不需要进行垃圾搜集”- 意味着分配成本与分配的内存大小是成比例的。分配的内存越少,GC需要做的就越少。如果需要15字节,就请求15字节而不会增加到32 字节或更大的块。分配有一个极限值,达到这个值就会触发垃圾搜集,应该尽可能的避免垃圾搜集。

GC堆区别与NT对的另一个特性就是:GC堆上的对象是一个紧挨一个的,因而可以保持当前的分配位置。

分配在GC堆上的每一个对象都需要8字节的额外开销(同步锁+方法表指针)。

如我前面提到的,大对象被区别对待,所以使用不同的分配模式。将在大对象这一节讨论此问题。

 

搜集

重要的事情优先 - 什么时候开始搜集(换言之,什么时候GC被触发)?GC在满足下面三个条件之一时触发:

1)  分配时达到0代极限值

2)  System.GC.Collect被调用

3)  系统处在低内存情况下

 

1)是最常见的。分配只发生在0代。每次垃圾搜集,0代被清空。然后0代被充满,接着再次搜集,如此反复。

不调用GC.Collect可以避免2) – 写应用程序的时候,一般情况下,永远不要调用它。BCL是唯一应该调用它的地方。在应用程序中调用GC.Collect带来的问题是:GC的次数比预计的要频繁(非常容易发生),性能急剧下降,这是由于为获得最佳性能而调节的GC调度被打乱了。

3)受到系统其它进程的影响,所以无法精确掌控,只能保证自己的进程行为良好。

 

看看这意味着什么。首先,GC堆是工作集(working set)的一部分。并且消耗私有页(private pages)。理想情况下,分配在0代上的对象总是在0代上死亡(对象总是被0GC搜集,永远不会发生完全搜集)所以GC堆讲永远不会超过0代的大小。现实中当然不是这样。所以应该保证GC堆大小是可控的。

其次,应该保证GC花费的时间是可控的。也就是说1)减少搜集次数 以及 2)减少对代龄高的代的搜集次数。对代龄高的代进行搜集比代龄低的代要昂贵的多,因为搜集高龄的代的同时也要搜集低龄的代。要么分配很快会死亡的临时对象(绝大多数在0代,搜集0代成本很低),要么分配存活很久的对象(呆在2)。对后一种情况,常见的场景是在程序启动时就分配一些对象 比如,订单系统中,为整个目录分配内存,这些内存直到应用程序终止才会释放。

CLRProfiler 是一个很棒的工具,可以用来查看GC堆上有什么以及什么在持有存活的对象。

 

如何组织数据

1)值类型 vs 引用类型

如你所知,值类型分配在堆栈上,引用类型在GC堆上。有人会问:如何决定什么时候使用值类型什么时候使用引用类型。就性能而言,答案通常是“看情况”,这里也不例外。值类型不会触发垃圾搜集,但如果值类型经常被装箱,装箱操作比创建引用类型的实例要昂贵;值类型参数在传递时需要拷贝。对于小成员来说,使用引用类型会带来额外的指针开销。我们有一些内部代码将其内联(比如,作为值类型)以缩小工作集来提升性能。这依赖于使用类型的模式。

2)引用富对象

如果对象是引用富集的(包含比较多的引用),会同时对分配和搜集带来压力。每个内嵌对象带来8字节开销。并且由于分配成本与分配大小成比例,,所以现在分配成本更高了。搜集时,构建对象引用树也需要花费更多时间。

 

一般建议根据类的逻辑设计来组织数据。对存活对象的引用,不需要时就不应该保持。举个例子,在可以避免的情况下,尽量不在老对象中储存对年轻对象的引用。

3)可终结对象(Finalizable objects)

我会在专门的章节详细讨论终结。但现在,最重要的事情是紧记可终结对象在被终结时,它持有所有对象都必须是存活的,这会增加搜集成本。所以要尽可能的将可终结对象与其他对象隔离开。

4)对象位置

在分配对象的子对象时,如果子对象的生存周期与父对象相同,那么它们应该被同时分配,它们将一起放在GC堆上。

 

大对象

在请求大于等于85000字节的对象时,将在大对象堆(LOH)进行分配。大对象堆永远不会压缩-仅仅清理(使用一个释放列表)。有一个不应该依赖的实现细节 如果分配大对象并且不希望被移动,必须确保钉住(pin)这个对象。

 

大对象仅在完全搜集时被搜集,所以搜集成本很高。有时会发现完全搜集之后2代对大小没有多大变化。这意味着搜集是针对大对象堆的 (可以在性能监视器中观察大对象堆的体积来判断)。

比较好的使用大对象的方式是:分配并重用对象。这样不会导致过多的完全搜集。如果对象大小在100k-120k之间,那么分配120K并重用它。分配大量临时大对象是个坏主意,因为这将导致总是进行完全搜集。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 国际物流查不到物流怎么办 纸币上印邪教该怎么办 钥匙掉到电梯缝里怎么办 汽车电子钥匙铜线折一根怎么办 防盗门的锁不好开怎么办 同学帮刷饭卡说不用还钱了怎么办 em231电源指示灯不亮怎么办 运行广联达卡住怎么办 马桶被粪便(大便)堵了怎么办 子宫壁厚12mm怎么办 管子太多每次洗澡都是冷水怎么办 热水冷水装反了怎么办 大树被高锰酸钾灌溉了怎么办会死吗 防盗门门被锁了怎么办 门被里面反锁了怎么办 门里面被锁了怎么办 被锁在门里怎么办 门锁住了没钥匙怎么办 车被别人锁住了怎么办 汽车轱辘被锁了怎么办 小车轮胎被锁了怎么办 国防光缆无明显标识被挖断怎么办 临工210挖掘机柴油进气怎么办 汽车抛光蜡干了怎么办 洗碗铁丝球吃了怎么办 牙套铁丝吃肚子怎么办 小铁丝吃到肚子怎么办 绿色抛光膏干了怎么办 不锈钢被盐酸弄黑了怎么办 不锈钢被草酸洗了怎么办 不锈钢洗菜盆被草酸腐蚀了怎么办 汽油发电机加了柴油怎么办 装载机发动机加入齿轮油怎么办 印尼的FromE错了怎么办 寄快递被弄坏了怎么办 福田口岸手表被扣怎么办? 网页显示与服务器连接失败怎么办 唯品会中发货无法清关怎么办 国际快递被海关扣了怎么办 我想开一家物流公司手续怎么办? 物流公司把我的货弄丢了怎么办