园子里有很多人已经对CLR的GC Heap有过激烈的讨论,里面有不少精华文章。但是既然是CLR系列,那么就不得不对GC Heap进行讲解。本文主要是对LOH(Large Object Heap)讲解。在一个托管进程被创建以后,在托管进程的内存空间里面,包含了System Domain,Shared Domain,Default Domain,Process的Heap,JIT 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的对象就被保存到这里。
segment begin allocated size
023d0000 023d1000 023d8e00 0x00007e00(32256)
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移动,请始终将其固定起来。