日期:2014-05-18  浏览次数:21098 次

[分享] 内存不足(OutOfMemory)的调试分析
原文地址:http://blog.csdn.net/lazyleland/article/details/6704661

32位操作系统的寻址空间是4G,其中有2G被操作系统占用,也就是说留给用户进程的内存只有2G(其中还要扣除程序加载时映像占用的部分空间,一般只有1.6G~1.8G左右可以使用)。

如果进程运行中需要申请内存,而操作系统无法为其分配内存空间,则会产生内存不足的异常,在.net中为System.OutOfMemoryException(The exception that is thrown when there is not enough memory tocontinue the execution of a program.)。

虽然最终的表现都为OutOfMemoryException,但其产生的原因可能是不一样的,动手解决此问题之前需要先对进程当前内存的使用状态进行分析,找出正确的原因,才能对症下药。下面分享一下调试此类问题的一些心得。

一、使用Perfmon.exe

1) 命令行输入perfmon.exe。打开“性能”。

2) 在“性能日志与警报-计数器日志”上右键,选择“新建日志设置”。

3) 输入日志名称,如“OOM”。

4) 在“常规-计数器”中删除所有默认的计数器(如果有)。

5) 点击“添加计数器”,性能对象选择“.NET CLR Memory”,计数器选择并添加“Bytes in all heaps”、“Large Object Heap Size”。同样“性能对象”选择“Process”,计数器选择并添加“Virtual bytes”、”Private bytes”。注意点击“添加”前需要在“从列表选择范例”选择框选择需要监控的进程。


另外,如果当前系统登陆的用例对目标进程没有调试权限,需要在“运行方式”框里填入domain\username,并输入密码。

6) 数据采样间隔可以设置小一点,如1秒钟。

7) 点击“确定“,新的计数器日志就新建成功了。右边的框框中可以看到新的计数器,绿色表示正在运行中。”“日志文件名“列显示了本次监控结果将写入的日志文件名(同一个计数器运行多次,写入的日志文件名是不同的)。


8) 让程序与计数器运行一段时间,然后停止计数器(为什么要停止计数器?我的机器上测试的时候,需要先停止计数器后,才会把监控的结果写到日志文件中,如果不先停止,在下面的监视器中将看不到计数器运行这段时间的监控结果。)。

9) 点击“系统监视器“。点击”“查看日志数据”(图标为)按钮,在“来源”选项卡里添加日志文件为刚刚我们新建的计数器产生的日志文件。下方可选择时间范围,这里选全部即可。然后在“数据”选项卡里添加需要查看的计数器(此选择卡还可以定义不同的计数器显示的样式及显示比例)。



10) 从图上可以看到在计数器运行的时间段中,被监控进程的内存使用情况。在添加计数器的窗口中有对相应计数器的简单说明,下面是几个常用的计数器:

· Bytes in all Heaps:.net托管堆(GC)使用的总内存。包括0代、1代、2代及大对象堆。

· Large Object Heap size:大对象堆使用的内存。.net在分配内存时大于85K的对象会被放到这个堆中,不同于0、1、2代,大对象堆中的内存不是连续的,在垃圾回收时也不会移动大对象的地址(我系统显示为大于20K对象为大对象,实际上2.0应该为大于85K)。

· Private bytes:该计数器记录了当前通过VirtualAlloc API Commit的Memory数量。无论是直接调用API申请的内存,被Heap Manager申请的内存,或者是CLR 的managed heap,都算在里面。跟Handle Count一样,如果在整个程序周期内总体趋势是连续向上,说明有MemoryLeak(摘自百度)。

· Virtual bytes:该计数器记录了当前进程申请成功的用户态总内存地址,包括DLL/EXE占用的地址和通过VirtualAlloc API Reserve的Memory Space数量,所以该计数器应该总大于Private Bytes。一般来说,Virtual Bytes跟Private Bytes的变化大致一致。由于内存分片的存在, Virtual Bytes跟Private Byes一般保持一个相对稳定的比例关系。当Virtual Bytes跟Private Bytes的比例关系大于2的时候,程序往往有比较严重的内存地址分片(摘自百度,但对.net程序来说一般差别在200M以下还算是正常的)。

11) 有了上面几个计数器的结果之后,一般可以通过以下规则大致定位问题的所在:

· Virtual bytes增长但Private bytes没有显著增长。为Virtual bytes泄露。

· Private bytes增长但bytes in all heaps没有显著增长。为非托管资源泄露,检查有没有COM组件或其它非托管调用没有正确释放内存。

· Bytes in all heaps显著增长。为.net托管内存泄露。由于.net内存是GC管理的,自动回收,这里有可能是缓存了过多的数据,或程序中引用混乱导致本来需要被回收的数据还被其它对象所引用从而GC没法回收这部分数据。

· Bytes in all heaps有增长但使用不多,系统剩余可用内存也比较多(需要再添加相应的计数器)。这种情况比较少见,但我遇到过一次是由于非托管在存在大量碎片,导致.net在申请大对象时失败。

二、使用Windbg

如果是由于.net托管内存导致的内存泄露,可以用Windbg进一步排查问题(非托管的也可以,但还没有对这方面进行详细研究过:))。

1) 加载SOS.dll。

.loadC:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll

2) 保存进程的映像文件。

.dump /ma “c:\oom.dmp”

3) 查看内存的使用情况。

!address –summary


RegionUsageIsVAD:VirtualAlloc的内存,一般为GC占用。

RegionUsageFree:可用内存。

RegionUsageImage:加载dll或exe占用的内存。

RegionUsageStack:线程堆栈占用的内存(.net中如果一个递归函数有问题导致无限循环调用会产生StackOverflowException)。

其它的可以参考Windbg文档,或打!address -?获得命令说明。另外上面有一个重要的信息,即Largest free gegion,我这里关心其size为18280KB,即是说当前可申请的最大连续内存块为18M多,也意味着如果此时进程去申请大于此数值的内存,也会报OutOfMemory(尽管目前Free的内存总共还有400多M,打!address –RegionUsageFree可以看到这400多M的内存的分块情况),通常引起此问题的原因,可能是非托管调用引起的严重内存碎片,因为托管的内存是连续的。由于大对象申请失败的问题调试,后面还会再进一步详细说明。

4) 查看托管堆内存的使用情况。

!eeheap –gc


上面显示了GC各个代及大对象堆的大小及每个段(segment)的大小、地址范围等等信息。GC在分配内存的时候是按段申请,按段释放的,也就意味着,GC占用的内存要比你的程序中为对象实际申请的总内存要大一点,如果程序为对象申请一块内存,而当前段的最大可用内存不足以分配时,GC为向系统申请新的段,从上面看到段的大小为16M左右,应该是按某种算法得出新段的大小(比如当前可用内存,操作系统或.net framework的版本等,只是我的猜测,有兴趣的童鞋自己查查文档后告诉我:))。

5) 查看当前托管堆中的对象,及每种对象占用的内存大小。

!dumpheap –sta