代码改变世界

【简译】大对象堆压缩:你该不该使用?

2013-10-24 18:26  muzinian  阅读(796)  评论(1编辑  收藏  举报

尽管.NET的自动内存管理有很多好处,但仍有一些危险需要我们避免。最常见的一点就是,频繁的处理大对象堆(the large object heap)的碎片问题。在这篇文章中,作者介绍了什么是LOH碎片,为什么它是一个问题,你如何避免它。

当我们讨论.NET 的堆内存时,我们自然会画一大块连续内存作为堆。然而,这个只是为了优化性能而仔细考虑过的架构,它有点不正确。.NET把堆分成4个分开的chunk,前三个是小对象堆(small object heaps (SOHs)),分别带代表0,1,2代(generation)。我们将关注第四个堆,也就是大对象堆(LOH)。它存储那些超过85000字节的对象。

简单概括.NET 内存

     如果你已经了解分代垃圾收集器,可以跳过这一段。如果你是新手,留下阅读以下。把内存按这样的方式分开是为了减小垃圾收集器(gc)的性能开销。经验表明:在实际应用程序中,事情趋向于最近创建的对象最有可能被销毁,这意味着这个现象有利于gc收集最近分配的对象比已经存在个一段时间的对象更频繁。

     通过把SOH分成三个不同代,使得gc可以不用在每次回收发生时都扫描所有内存而只是收集SOHs的几个部分(降低了性能开销)。简短的说,当一个新对象在SOHs上实例化后,它本放在了0代上(generation 0)。如果经历过一次gc和没有被回收它就被“提升”为1代(generation 1),如果经历过第二次gc而没有被回收它就被提升为2代(generation 2)。

     上面有一点简化,有些对象可能会留在当前的代中(generation)因为它们可以被固定住,或者添加到finalizer queue,或者当垃圾收集本身时创建。

      当generation 0 装满时0代收集(generation 0 collection)将会发生,而当generation 1和generation 0都装满时1代收集(generation 1 collection)将会开始。类似的

2代收集(generation 2 collection)也会收集低级别代,这是一个相对较贵的行为。还好,CLR在运行时会跟踪你的应用程序的内存分配器,为了更好的性能不断地调整各种代的大小,同时,也决定何时开始generation 2 collection。在收集结束之后,每个留在SOHs的对象是被压缩过的,这意味着他们为了删除在内存之间的间隙而相互挨着。这意味着CLR可以只分配它实际需要的内存,而不是尝试填充新的提升过的对象到笨拙的固定尺寸的间隙(也叫碎片)。

(这段翻译的不是很流畅,给出原文:A generation 0 collection will happen when generation 0 is full, and a generation 1 collection will happen when generation 1 is full and will also collect generation 0. Similarly generation 2 collections also collect all lower generations, and thus is relatively expensive to do. Thankfully, the CLR tracks your application’s memory allocations at run time and continually tunes the size of the various generations for maximum performance, and also decides when to perform generation 2 collections. After a collection, any remaining objects on the SOHs are ‘compacted’, meaning they are shuffled up against each other to remove any gaps in memory. This means that the CLR can allocate only as much memory as is actually needed, rather than try and fit into new and promoted objects into awkwardly sized gaps (known as fragmentation).

当generation 2 collection发生时,LOH也被收集,但是,不想SOHs,当垃圾收集时LOH不被压缩,这意味着,LOH可能进入分散的状态。如果一个队被碎片分割的过多,他就没有足够大的空隙给将要分配空间的新对象,因此,新对象将添加到堆的后端,也就导致堆的膨胀。这是一个问题。如果这个过程不断重复,LOH最终会耗尽系统可用内存,程序将会以OutOfMemory异常而崩溃。

点此了解跟多

 为什么LOH碎片如此的坏

      因为.NET抽象了物理内存地址的概念,LOH碎片可能会导致难以处理的问题。这使得开发者很难发现哪里是CLR可能分配内存的地方,甚至更加困难的是,找到哪个特别的分配方式导致了LOH存在空隙。为了是故障排解更难(这句不知道作者的意思),程序运行很长的时间才可能显示出潜在的问题。着导致调试成为了乏味的过程。      更多LOH碎片的危害,点此了解

你要如何做才能避免LOH碎片

     通常,解决LOH碎片问题有以下三个策略:
     1)找到导致碎片的大对象,把它们分割成小一点的具有对等功能的包装类;

     2)重新设计应用程序,减少大对象;

     3)定期重启应用程序(这本质上是ASP.NET应用的回收应用程序池寻求的目标)

每个方法要么困难,要么不雅,要么费劲,要么就是这些组合在一起。然而,在.NET 4.5.1 Microsoft .NET团队提供了另一个可选项,在LOH压缩之后,增加一个一次性的gc,通过以下代码:

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; 

GC.Collect(); // This can be omitted

如果GC.Collect() 本省略了,LOH压缩将会在下一个LOH gc 发生时自然地开始。在这个修改后的gc完成之后,应用程序会像以前一样继续运行(例如,没有 LOH压缩)

     跟SOH不同,在gc发生时,微软谨慎的选择不要通过默认方式来压缩LOH。他们认为定期的实行LOH压缩带来的性能冲击会超过这样做带来的好处。他们给出了关于使用LOH压缩的建议:

     LOH压缩会成为一个昂贵的操作,必须经过严格的性能分析之后在再使用,我们都认为LOH碎片是一个问题,但也决定何时要求压缩。

     我将展示在LOH压缩期间更多的细节,阐述何时使用它。

压缩会持续多长时间

      为了研究LOH压缩的性能冲击,我写了一个简单的.NET4.5.1测试程序。在随机大小的大对象[84kb,16mb]中,实例化一个随机数[60,140],然后删除一个随机抽样,着导致了LOH进入了碎片状态(instantiates a random number (100±40) of  randomly sized large objects (84KB <= size < 16MB) , and then subsequently removes a random selection of them, thereby leaving the LOH in a fragmented state.)。

     我们通过比较使用了标准GC的时间和使用了带有LOH压缩的GC的时间,两者之差就可以估计压缩的时长,这就可以推断出LOH压缩的持续时间。为了让这个办法可用,堆在每次试验之前都必须是一致的,因此,我保证了实例化相同的随机选择的对象,然后在每次实验之前进行一次全GC。

     重复20次这个过程,如下图:

Results from LOH compaction

      数据必须移动的数据数量和LOH压缩时长,是呈现线性相关的。为了量化移动的数据的数量,我们必须考虑在压缩中,发生了什么。

压缩算法

     在压缩期间,压缩算法将查询LOH直到他找到一个空隙,在此点,它将会取堆上下一个对象简单的移动它去填这个空隙。它将会持续查找这个堆,在每次它遇到空隙,它不断地转移对象。如果这个空隙靠近LOH开始的地方,则在LOH上大部分数据将会移动。

     实际结果是,压缩有一点碎片的堆和很多碎片的堆将会移动一样多的数据,因此会耗费大致相同的时间。着意味着,执行频繁的压缩不会使得后续的压缩更快。因此,你应当直到必须执行压缩时,才是用它。 

     LOH压缩和SOH压缩都有的一个有趣的功能是,在压缩时,堆上的对象不会重排。尽管这样做会提高压缩的速度。原因是为了保护局部性原理,可能被创建的对象以相似的顺序被访问。另外,计算合适的顺序的时间将会抵消掉潜在的时间节约。

你要何时使用压缩

     我建议在以下条件被满足的情况下再使用LOH压缩:

     1)你的目标已经是,NET或者准备升级到它

     2)上述估计的暂停时间不会影响你的应用的使用

     3)不可能进行打破大对象到小的块,或者减小大对象的生成

      这是一个有用的功能,但是,这应该是最后的策略。

(下面是介绍ANTS Memory Profiler 8 这款收费软件的使用,就不翻了)