【译】让垃圾回收器高效工作(一)

如何提高垃圾回收的工作效率

这篇文章的目的是介绍如何更有效率的使用托管内存,而不是介绍GC本身的,是解释如何使用GC。我假定多数读者都对如何用好GC感兴趣,而不是自己实现一个GC。这篇文章需要对GC有一个基本的了解,Jeff Richter写了两篇很棒的文章介绍GC,我翻译了这两篇文章,如果你尚对GC一点都不了解,建议先看下垃圾回收原理12.

首先我会专注于Workstation GC(因此所有的数字都是工作站GC的)。然后我会谈谈工作站GC和服务器GC之间的区别(有时候你没有必要选择,稍后我会解释为什么)。

代:

把托管堆上的对象分成3代是为了调优垃圾回收的性能,大多数对象都在0代时消亡。例如:在一个服务器程序中,处理每个请求相关的对象,都会在请求完成后消亡。本质上1代对象是在新分配对象和常驻内存之间的一个缓冲区。当你在性能计数器中观察2代回收发生的次数比0代回收次数要少的多。而1代回收次数相对来说不是很重要,回收1代对象比回收0代对象的代价高的不是很多。而回收2代对象就意味着要扫描整个托管堆了,代价相对要大得多。

GC段(segment):

首先让我们看一下GC是如何向操作系统申请内存的。GC以段的方式保留内存。每一个段是16M(服务器模式下可能是64M)。当执行引擎启动时,我们保留初始的GC段,一个给小对象用,另一个段给大对象用。有关大对象堆的垃圾回收请参考这里

在需要的时候可以向操作系统申请更多内存,或者交还给操作系统。当所有段都用完之后我们就申请一个新段。在每一次完整的垃圾回收之后多余的段会交还给操作系统。

大对象有自己的段,垃圾回收器对大对象的处理方式和小对象是不一样的所以大对象不和小对象共享段。

分配:

当你在托管堆上分配一个对象时,要付出什么代价呢?如果我们不考虑回收的话,有两点1是向前移动指针,2是为新对象清空内存。而对于实现Finalize的方法的对象还需要把对象的指针放到终结队列中。

注意我说的是“如果我们不考虑回收”—这意味着分配的代价和对象的大小成正比。申请的越少,GC的代价就越小。如果你需要15个byte,就申请15个字节;不要像使用maalloc一样申请32个字节。有一个阀值,当超过这个值时,就会触发垃圾回收。你要尽可能少的触发垃圾回收。

GC堆和NT堆还有一点不同:分配对象的时间越接近,对象在GC堆上的也越接近。

在GC堆上分配的每一个对象都需要额外的8byte的开销,4byte用来同步,4byte存放方法表指针。

回收:

首先我们要知道什么时候触发回收? 有如下三种情况会触发:
1. 分配时超过了0代堆的阀值
2. 调用了GC.Collect()方法
3. 操作系统给应用程序发出低内存信号

第1种情况是最典型的触发原因,当分配的对象足够多时,就会触发0代堆的垃圾回收。在每一次回收之后,0代堆就清空了。然后继续分配对象,0代堆填满之后就会触发下一次回收。

你要尽量避免第2种情况,这个很简单,不要在程序代码中调用GC.Collect方法就可以了。通常情况下你不应该调用Collect方法。BCL is basically the only place that should call this (in very limited places);当你在程序中调用GC.Collect方法时,性能会降低,因为回收提前执行了,而垃圾回收器执行回收的调度是经过算法优化的。

第3种情况受操作系统上运行的其他程序影响,这个你的程序没法控制,你只能尽可能的优化好你的程序或模块。

让我们谈一下这意味着什么。首先,托管堆是程序的工作集的一部分。它会消耗私有页。在理想情况下,所有对象都在0代时消亡(这意味着,几乎所有对象都在0代回收,完全回收从不会发生)因此,你的GC堆永远不会超过0代堆的大小。而事实上这种情况是不可能的,因此,你真的需要保证托管堆的大小是可控的。

第二,你需要保证垃圾回收消耗的时间资源是可控的。这个意思是一要尽可能少触发GC,二尽可能少发生高代的GC。一次高代的回收要比底一代回收的代价高得多,因为高代的回收要扫描更多的对象,要同时执行所有更低代的回收。

CLRProfiler是一个观察GC堆看堆上的对象被那个对象引用的工具,它非常棒。

如何组织你的数据:

1) 用值类型还是引用类型

如你所知,值类型数据是存放在栈上的,而引用类型对象是存在托管堆上的。因此,人们会问,如何决定什么时候使用值类型,什么使用引用类型呢。值类型不会触发垃圾回收,但是如果你的值类型经常做装箱操作,装箱操作要比刚开始就创建一个引用类型对象要昂贵的多;当值类型对象作为参数传递时需要复制一份。但是如果你的引用类型只有一个小成员如果做成引用类型的话,还需要额外4字节的指针开销和同步开销以及方法表开销。因此该使用值类型还是引用类型是由类型本身决定的。

2) 富引用对象(Reference rich object)
如果一个对象是富引用的,会给分配和回收都带拉压力。每一个内嵌的对象都需要8字节的额外开销。因为分配的开销和对象的大小是成正比的,所以开销就大了一些。另外富引用会导致构建对象图的时间增大,增加了回收的开销。

因此我建议你设计对象时只设计必要的字段,如果对另外一个引用类型的强引用不是必须的,就不要引用它。你应该尽量避免让已存在很长时间的对象引用新分配的对象。

3) 可终结对象(实现Finalize方法的对象)
如垃圾回收原理1中所述终结对象会延长回收的时间,不仅延长可终结对象本身的,还会延长它的引用链下游的所有对象的回收时间。所以如果对象必须是可终结的,你就要尽可能的隔离它,不让它引用其他对象

4) 对象的存储位置:
当你为一个对象的子对象分配空间时,你最好在同一时间分配父对象和子对象,这样父子对象在托管堆上的地址就会在一起,回收起来也会一起回收,回收的效率就会相对高一些。

大对象:

当一个对象占用的内存超过85,000bytes时它就会被分配到LOH上。SOH段永远都不会做移动—而只是清空对象(使用一个空的链表)。但是这个情况是一种实现的细节,你不应该依赖这个实现细节。如果你分配了一个大对象,不希望他发生移动,那么你应该fix它。

只有在2代回收时才会做大对象的回收,而2代回收的代价是很大的。有时候你会发现2代回收之后2代堆的大小并没有发生多大变化,这有可能是因为大对象堆大小超过阀值触发了2代回收。

一个好的实践:分配一个大对象然后重复利用它。如果说你需要一个100k或者120k的大对象,你应该申请一个120k的然后重复利用它。多次分配临时大对象可能会触发2代回收,对性能会有负面影响。


此文是翻译文章:
原文地址:http://blogs.msdn.com/b/maoni/archive/2004/06/15/156626.aspx
原作者:Maoni Stephens

 

相关随笔:

.Net 垃圾回收机制原理(一)

.Net 垃圾回收机制原理(二)

.Net 垃圾回收和大对象处理

posted @ 2011-11-28 08:42  玉开  阅读(4369)  评论(0编辑  收藏  举报