这篇文章我们来谈谈垃圾回收器和程序的虚拟内存、物理内存之间的关系。再谈谈怎样判断你的托管堆是否是健康的;为什么在机器还有大量内存的情况下程序会抛出OutofMemoryException。
垃圾回收和物理内存虚拟内存之间的关系:
如果你对这个话题已经了如指掌,请跳过这一段。
GC需要分配段,有关段的解释请参考《让垃圾回收器高效工作(一)》。GC调用VirtualAlloc来分配段空间。这意味着如果你的进程中没有足够的连续空间,分配就失败了。这是一种GC抛出OutOfMemeoryException的合法的情况(精确的说,GC没有抛出异常,抛出异常是执行引擎干的,GC只是在分配失败时返回了NULL)。
经常有人问我这样的问题:“为什么我程序的托管堆只使用了X MB的内存,运行时抛出了OutofMemoryException呢?” 其中XMB比2GB小得多。
记住在.Net程序中有一些内存不是GC消耗的。GC和其他一些东西一样是在竞争虚拟内存的空间。比如:你进程中载入的模块需要占用虚拟内存;有些模块直接调用本机代码分配内存也会消耗虚拟内存(VirtualAlloc,HeapAlloc,C++的new等等)。CLR本身也会有些不通过GC分配消耗的内存,比如jitted代码,一些CLR需要的数据结构等等。通常CLR需要的内存是相当小的。你可以通过SOS的!eeheap命令来查看CLR使用的内存信息。下面是运行此命令的一个输出样例,一些说明用中括号括起来了。
0:119> !eeheap
Loader Heap:
--------------------------------------
System Domain: 79bbd970
LowFrequencyHeap: Size: 0x0(0)bytes.
HighFrequencyHeap: 007e0000(10000:2000) Size: 0x2000(8192)bytes.
StubHeap: 007d0000(10000:7000) Size: 0x7000(28672)bytes.
Virtual Call Stub Heap:
IndcellHeap: Size: 0x0(0)bytes.
LookupHeap: Size: 0x0(0)bytes.
ResolveHeap: Size: 0x0(0)bytes.
DispatchHeap: Size: 0x0(0)bytes.
CacheEntryHeap: Size: 0x0(0)bytes.
Total size: 0x9000(36864)bytes
--------------------------------------
Shared Domain: 79bbdf18
...
Total size: 0xe000(57344)bytes
--------------------------------------
Domain 1: 151a18
...
Total size: 0x147000(1339392)bytes
--------------------------------------
Jit code heap:
LoaderCodeHeap: 23980000(10000:7000) Size: 0x7000(28672)bytes.
...
Total size: 0x87000(552960)bytes
[jited代码消耗的空间非常小 –这是一个相当大的程序]
--------------------------------------
Module Thunk heaps:
Module 78c40000: Size: 0x0(0)bytes.
...
Total size: 0x0(0)bytes
--------------------------------------
Module Lookup Table heaps:
Module 78c40000: Size: 0x0(0)bytes.
...
Total size: 0x0(0)bytes
--------------------------------------
Total LoaderHeap size: 0x1e5000(1986560)bytes [total Loader heap takes < 2MB]
=======================================
Number of GC Heaps: 4
------------------------------
Heap 0 (0015ad08)
generation 0 starts at 0x49521f8c
generation 1 starts at 0x494d7f64
generation 2 starts at 0x007f0038
ephemeral segment allocation context: none
[The first 2 segments are read only segments for frozen strings which is why they look a bit odd compared to other segments. The addresses for begin and segment are very different and usually they are tiny segments (unless you have tons and tons of frozen strings)]
segment begin allocated size
00178250 7a80d84c 7a82f1cc 0x00021980(137600)
00161918 78c50e40 78c7056c 0x0001f72c(128812)
007f0000 007f0038 047eed28 0x03ffecf0(67103984)
3a120000 3a120038 3a3e84f8 0x002c84c0(2917568)
46120000 46120038 49e05d04 0x03ce5ccc(63855820)
Large object heap starts at 0x107f0038
segment begin allocated size
107f0000 107f0038 11ad0008 0x012dffd0(19791824)
20960000 20960038 224f7970 0x01b97938(28932408)
Heap Size 0xae65830(182868016)
------------------------------
Heap 1 (0015b688)
...
Heap Size 0x7f213bc(133305276)
------------------------------
Heap 2 (0015c008)
...
Heap Size 0x7ada9ac(128821676)
------------------------------
Heap 3 (0015cdc8)
...
Heap Size 0x764c214(124043796)
------------------------------
GC Heap Size 0x21ead7ac(569038764) [托管堆消耗的内存大约为540MB]
程序在这一刻发生了OutOfMemoryException。让我们看下这一个时间点的空闲虚拟内存块。你可以使用!vadump命令或者其他一些工具来分析虚拟内存使用情况。我非常喜欢用!address命令,它可以很好的输出最大的空闲区。
0:119> !address
...
Largest free region: Base 54000000 - Size 03b60000
0:119> ? 03b60000
Evaluate expression: 62259200 = 03b60000
可以看到最大的空闲区域小于64M,这个就是发生OOM的原因了。垃圾回收器需要分配一个新的段,但是却没有足够的空间了(这个程序是运行在Server GC模式下的,所以一个段是64M)。
如果你在程序中经常载入或者卸载不同大小的模块虚拟内存空间会有很多碎片,这很糟糕。比如:COM dlls在不用时卸载,然后过一会又重新载入;web程序有很多小的dll(一个10k的dll需要占用64k的虚拟内存)。
除了分配段之外垃圾回收器和虚拟内存就没有任何关系了。而物理内存就是另一回事儿了。如我在前文提到的,如果你的机器的物理内存很低就会触发垃圾回收。这时候垃圾回收器变得很有侵略性。在物理内存低的时候GC性能计数器0代,1代,2代回收次数之间的比例会接近1. 如果GC没有足够的内存来满足分配请求也会抛出OutofMemoryException。
现在机器有超过2G的内存已经不算什么了。不要过分的碎片化虚拟内存是很重要的。然而,如果如果你的程序运行在64位服务器上,虚拟内存空间很大,而物理内存有可能会成为限制因素。
你的托管堆是正常的吗?
我们先来看看如何获得托管堆性能数据:
1. 收集性能计数器日志数据
我建议你在使用GC性能计数器时要每隔一个很小的间隔(比如1秒)记录一次数据,然后连续收集几分钟。通常情况下这要比收集几个小时,每隔5-10秒收集一次数据得到的样本更有意义。然而,如果你托管程序的问题是随机出现的,那没有选择你必须要长时间收集性能计数器数据。
2. 通过生成dump文件分析
如果要分析托管堆的问题最好抓一个完整的dump,mini dump通常情况下是没用的
3. 使用CLRProfiler观察
CLRProfiler是一个很重的工具,它不能附加到进程。有关如何使用这个工具,请看它的文档,文档中有很详细的说明,还有几个测试用的例子
当你观察托管堆时,收集什么样的数据是有意义的呢?
1. Time in GC
如果%Time in GC的值很小,那么这个参数对分析分配相关的问题是没用的。当%Time in GC的值很大通常意味着程序做了太多的2代回收并且每次都花了很长时间。这时候就该详细的分析一下你的代码找出为什么会有这么多的2代回收,是否有办法改变分配的模式来减少2代回收的时间了。
2. 托管堆增长模式
如果你有一个需要长时间运行的服务应用。如果你观察到托管堆不停的增长,那通常是一个坏兆头。通常情况下服务程序要把每一个处理完的请求相关数据都清理掉,如果不是这样的话那你的程序很有可能有内存泄漏的情况,你必须查一下原因了---否则程序会报OutofMemoryException。出现这种情况时我们会在第一时间要求开发人员去修复。
观察性能计数器也可以发现一些明显的问题,比如:如果# of GC Handles持续增长,就表明你的程序中肯定存在一个句柄泄漏。
3. 各代回收次数之间的比例
我经常告诉人们一个健康的托管堆2代回收次数和1代回收次数之比是1:10;如果你发现他们的比例是1:1,就需要看看你的程序了。如果托管堆很大,做一次2代回收就需要花费很长时间了,理想状况下我们希望2代回收尽可能的少。
4. 碎片(Fragmentation)
碎片是托管堆上的空闲空间,你可以通过下面的sos命令查看它:
!dump –type Free –stat
当然程序中的碎片越小越好,但是这几乎不可能。因为只要你使用IO或者其他可能固定内存地址的对象就有可能在托管堆上形成内存碎片。不同代上的碎片对程序性能的影响是不同的:
0代堆上出现碎片是好的,因为我们需要在0代堆上分配对象,在分配对象时会占用这些碎片。程序碎片都出现在0代堆上是一种非常理想的状态。你可以为!dumpheap命令指定开始和结束地址来查看0代堆上有多少空间,我们看下前面例子dump的情况:
Heap 0 (0015ad08)
generation 0 starts at 0x49521f8c
generation 1 starts at 0x494d7f64
generation 2 starts at 0x007f0038
ephemeral segment allocation context: none
segment begin allocated size
00178250 7a80d84c 7a82f1cc 0x00021980(137600)
00161918 78c50e40 78c7056c 0x0001f72c(128812)
007f0000 007f0038 047eed28 0x03ffecf0(67103984)
3a120000 3a120038 3a3e84f8 0x002c84c0(2917568)
46120000 46120038 49e05d04 0x03ce5ccc(63855820) [the last one is always the ephemeral segment]
0:119> ? 49e05d04-0x49521f8c
Evaluate expression: 9321848 = 008e3d78 [gen0 is about 9MB]
0:119> !dumpheap -type Free -stat 0x49521f8c 49e05d04
------------------------------
Heap 0
total 409 objects
------------------------------
Heap 1
total 0 objects
------------------------------
Heap 2
total 0 objects
------------------------------
Heap 3
total 0 objects
------------------------------
total 409 objects
Statistics:
MT Count TotalSize Class Name
0015a498 409 7296540 Free
Total 409 objects
从上面的输出可以看到0代堆的大小约为9M,其中7M空闲空间。
可以说大对象堆上出现碎片是设计设然,因为我们不会在大对象堆上做碎片整理。这不意味着在大对象堆上分配对象和使用NT 堆管理器的分配方式相同。根据GC的工作特性,被释放的对象会调整到一起组合成一个大的空间可以用来满足大对象分配请求。
碎片出现在2代堆和1代堆上的情况是最糟糕的。如果在一次垃圾回收之后1代堆和2代堆上仍然有很多空闲内存就可以肯定程序在托管内存的使用上是存在问题的。如果你的程序出现这种问题,请参考《让垃圾回收器高效工作三》
对于2代堆来说如果碎片比例小于20%就可以认为是非常好的。
原文:http://blogs.msdn.com/b/maoni/archive/2005/05/06/415296.aspx
原作者:Maoni Stephens
相关随笔: