CLR系列:大型对象堆

园子里有很多人已经对CLR的GC Heap有过激烈的讨论,里面有不少精华文章。但是既然是CLR系列,那么就不得不对GC Heap进行讲解。本文主要是对LOHLarge Object Heap)讲解。在一个托管进程被创建以后,在托管进程的内存空间里面,包含了System DomainShared DomainDefault DomainProcessHeapJIT Code heap(都包含在LoaderHeap)GC Heap以及LOH

 

CLR 垃圾回收器 (GC) 将对象分为大型、小型两类。如果是大型对象,与其相关的一些属性将比对象较小时显得更为重要。例如,压缩大型对象(将内存复制到堆上的其他位置)的费用相当高。我将讨论符合什么条件的对象才能称之为大型对象,如何回收这些大型对象,以及大型对象具备哪些性能意义。

 

大型对象堆

.NET Framework 1.1 和 2.0 中,如果对象大于或等于 85,000 字节,将被视为大型对象。注意:此数字根据性能优化的结果确定。当对象分配请求传入后,如果符合该大小,便会将此对象分配给大型对象堆。为什么会这样呢?我们先看看NET 垃圾回收器的基础知识。

 

我们知道,.NET 垃圾回收器是分代回收器。它包含三代:第0代、第1代和第2代。之所以分代,是因为在良好调优的应用程序中,您可以在第0代清除大部分对象。每当触发一次垃圾回收,NET就首先扫描第0代,仍存在的对象将被放到第1代,当第1代放满后,会对第1代进行垃圾回收,仍存在的对象将被放到第2代,以此进行下去。但是,最后一代回收未处理的对象仍会被视为最后一代中的对象。从本质上讲,第1代是新对象区域与生存期较长的对象区域之间的缓冲区。回收任何一代,都会回收他以前的所有代。例如,执行第1代垃圾回收时,将同时回收第1代和第0代。执行第2代垃圾回收时,将回收整个堆。

加载 CLR 时,将分配两个初始堆栈段(一个用于小型对象,另一个用于大型对象),我将它们分别称为小型对象堆 (SOH) 和大型对象堆 (LOH)。然后,通过将托管对象置于任一托管堆栈段上来满足分配请求。如果对象小于 85,000 字节,则将其放在 SOH 段上;否则将其放在 LOH 段上。随着分配到各段上的对象越来越多,会以较小块的形式提交这些段。用户代码只能在第 0 代(小型对象)或 LOH(大型对象)中分配。只有垃圾回收器可以在第1代(通过提升第0代回收未处理的对象)和第2代(通过提升第1代和第2代回收未处理的对象)中“分配”对象。触发垃圾回收后,垃圾回收器将寻找存在的对象并将它们压缩。如果没有足够的可用空间来容纳大型对象分配请求,我会先尝试从操作系统获取更多段。如果失败,我将触发第 2 代垃圾回收以便释放一些空间。

如果大家还有所怀疑,可以看看下面windbg+sos来看看一个用户态运行的程序的GC Heap里面都是些什么:

Number of GC Heaps: 1
generation 0 starts at 0x013d1018
generation 1 starts at 0x013d100c
generation 2 starts at 0x013d1000
ephemeral segment allocation context: none
 segment    begin allocated     size
001b09f8 7a733370  7a754b98 0x00021828(137256)
001b3fc0 7b463c40  7b47a744 0x00016b04(92932)
0014df68 790d8620  790f7d8c 0x0001f76c(128876)
013d0000 013d1000  01425ff4 0x00054ff4(348148)
Large object heap starts at 0x023d1000
 segment    begin allocated     size
023d0000 023d1000  023d8e00 0x00007e00(32256)
Total Size   0xb488c(739468)
------------------------------
GC Heap Size   0xb488c(739468)
我们一个GC Heap里面有四个Heap Segment,紧接着Heap Segment的,是LOH。这些Generation是保存在一个一个的Heap Segment里面的。一个Heap Segment可以包含两个Generation或者三个,或者更多。而一个Generation可以跨越多个Heap Segment紧接着GC Heap的内存区域,就是LOH。在默认情况下,超过85000byte的对象就被保存到这里。

 

何时回收大型对象
分配超出第 0 代或大型对象阈值
调用 System.GC.Collect 如果对第 2 代调用 GC.Collect,将立即回收 LOH 及其他托管堆。
系统内存太低
 
LOH 性能
CLR选择扫过所有对象,扫描一遍来判断这个对象有没有对别的对象的引用。如果有的话,这一类对象就会存储在一起,而没有对其它对象引用的对象就会存在其余的另外一个区域。这样做是基于性能考虑的。
们来看一下回收成本。前面曾提到,LOH 和第2代将一起回收。如果超过两者中任何一个的阈值,都会触发第2代回收。如果由于第2代为 LOH 而触发了第2代回收,则第 2 代本身在垃圾回收后不一定会变得更小。因此,如果第2代中的数据不多,这将不是问题。但是,如果第2代很大,则触发多次第2代垃圾回收可能会产生性能问题。毫无疑问,如果仍继续分配和处理真正的大型对象,分配成本肯定会大幅增加。
我们可以看看上面的调试结果,我们可以看到
Large object heap starts at 0x023d1000
 segment    begin allocated     size
023d0000 023d1000  023d8e00 0x00007e00(32256)
您会看到 LOH 的总大小少于 85,000 个字节。为什么会这样?这是因为运行时本身实际使用LOH分配某些小于大型对象的对象。在看看下面的调试代码(LOH的内存段)
0:011> !dumpheap -stat 023d1000  023d8e00
total 17 objects
Statistics:
      MT    Count    TotalSize Class Name
0014c090        9          144      Free
7912d8f8        8        32112 System.Object[]
Total 17 objects

大型对象费用很高。由于 CLR 需要清除一些新分配大型对象的内存,以满足 CLR 清除所有新分配对象内存的保证,所以分配成本相当高。LOH 将与堆的其余部分一起回收,如果可以,建议重新使用大型对象以避免托管堆。LOH 上的特大对象通常是数组(很少会有非常大的实例对象)。如果数组元素包含很多引用,则成本将会很高。如果元素不包含任何引用,则根本无需处理此数组。最后,到目前为止,在回收过程中尚不能压缩 LOH,但不应依赖于此实现。因此,要确保某些内容未被GC移动,请始终将其固定起来。

posted on 2008-07-23 09:46  gjcn  阅读(2313)  评论(12编辑  收藏  举报

导航