内存不足(OutOfMemory)的调试分析

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 –stat

从上图中看到最大的类型为字符串,共占用了135M内存。

6)   查看某种类型的所有实例地址。

!dumpheap -mt 793308ec

7)   查看某个对象的信息。

!do [对象地址]

对象地址可以在!dumpheap –mt命令的第一列中得到。

8)   查看某个对象占用的内存大小。

!objsize [对象地址]

如果对象引用了其它对象,此命令会把其引用的其它对象占用的内存也算进去。

9)   查看数组中的元素。

!dumparray [数组对象地址]

如果用!do得到的对象为数组,用此命令得到数组中每个元素的地址,再用!do打出数组元素的信息。

10) 查看对象与其它对象的引用情况。

!gcroot [对象地址]

这个命令在判断.net托管内存泄露很有用,它可以得到某个对象没有被GC释放掉的原因(因为存在根对象的引用关系)。

11) 查看对象大小大于某个数值的所有对象。

!dumpheap –min 10000000

12) 查看大对象堆的对象。

!dumpheap –startatlowerbound [大对象堆的起始地址]

大对象堆的起始地址可以由命令!eeheap –gc得到。

13) 调试由大对象内存分配不足引起的OutOfMemory。

有些情况下,明明内存还剩下很多,但是由于非托管带来的内存碎片,导致连接内存不足以分配程序申请的大对象的内存,这时也会报内存不足的异常。要确定内存不足是否由此原因引起的可按以下步骤调试:

在程序申请大对象的时候,用windbg打一个断点,并把大对象申请的内存的大小打印出来。

0:027>x mscorwks!WKS*allocate_large*

79f7d9ebmscorwks!WKS::gc_heap::allocate_large_object = <no type information>

0:027>bp 79f7d9eb "[email protected];!clrstack"

如果程序申请大对象,会有类似下面的输出,大对象的大小为52M。

Evaluateexpression: 52679596 = 0323d3ac

此时可以在输出里看到堆栈,确定是程序哪个代码需要申请这么大的对象,是否属于正常。也可以用!address –summary看当前可申请的最大连接内存块大小,如果小于待申请的大对象大小,则会出现内存不足。

另外,在以前的调试中我得到这么一个结论,如果程序声明了一个长度大于85000/4=21000的数组,这时数组实际占用的内存大于85K,GC会把这个数组放在大对象堆中,对于List类型,其长度是可以动态增加的,如果长度从小于21000到达到21000,GC也会把它移到到大对象中(刚一开始长度小于21000时不在大对象堆中)。

14) 查看GC的终结队列及线程。

另一种导致托管内存没有被释放的原因(除了对象被引用)就是GC的终结线程被阻塞了,从而导致可以释放的对象来不及被释放。可以按以下步骤调试此类问题:

!finalizequeue

有类似以下的输出:

这里需要关注的是Ready for finalization XX objexts,表示终结队列中有多少个对象正在等待被回收,如果数量比较大,可以进一步看看终结线程的堆栈。

!threads

后面带有Finalizer的即终结线程。

~[线程号]s

!clrstack

根据堆栈信息,可以初步断定引起终结线程阻塞的代码位置,然后针对代码做进一步的分析。

转自:http://blog.csdn.net/lazyleland/article/details/6704661

时间: 2024-11-07 01:25:22

内存不足(OutOfMemory)的调试分析的相关文章

使用Perfmon.exe调试分析内存不足(OutOfMemory)

32位操作系统的寻址空间是4G,其中有2G被操作系统占用,也就是说留给用户进程的内存只有2G(其中还要扣除程序加载时映像占用的部分空间,一般只有1.6G~1.8G左右可以使用). 如果进程运行中需要申请内存,而操作系统无法为其分配内存空间,则会产生内存不足的异常,在.net中为System.OutOfMemoryException(The exception that is thrown when there is not enough memory tocontinue the executi

程序员突围-程序调试分析(一) 我从菜鸟进化的感悟

程序员突围-程序调试分析(一) 我从菜鸟进化的感悟 在说程序调试分析之前,我们还是了解一些基本的概念性的东西(在下现在从事java,因而都已java为例) 1. bug的分类 根据程序的阶段和MSDN和看过的一些书籍的分析,bug分为编译错误,运行时错误和逻辑的错误 (1)  编译错误 一般初学者犯错比较多的地方,编译错误,说白了就是程序在从java编译成.class文件时出现了问题,这个问题的现象比较明显,比如说语句写的有问题,那么对于这类问题的解决方法是什么呢,翻翻书,翻翻API(翻阅API

内存泄露 的可能原因分析!

如果系统内存消耗越来越大,CPU越来越高,可能性最大的是系统存在内存泄露. 是由于内存泄露导致的可用内存减少,当达到某一临界点的时候,会频繁导致虚拟机垃圾回收,而垃圾回收又是高CPU消耗操作,因此CPU使用率会上升. 这种问题最终造成的OutOfMemory,系统无法正常工作. sqlserver 启停: 重启IIS和Sqlserver的命令 iis:net stop iisadmin /ynet start w3svc 或者iisreset sqlserver:net stop mssqlse

jQuery之ajax错误调试分析

jQuery之ajax错误调试分析 jQuery中把ajax封装得非常好.但是日常开发中,我偶尔还是会遇到ajax报错.这里简单分析一下ajax报错 一般的jQuery用法如下,ajax通过post方式提交"汤姆和老鼠"这段数据到xxx.php文件中.成功后则打印返回的数据,失败则打印错误原因. 1 2 3 4 5 6 7 8 9 10 $.ajax({     url:"xxx.php",     type:"post",     dataty

DEBUG模式下, 内存中的变量地址分析

测试函数的模板实现 [cpp] view plain copy /// @file my_template.h /// @brief 测试数据类型用的模板实现 #ifndef MY_TEMPLATE_H_2016_0123_1226 #define MY_TEMPLATE_H_2016_0123_1226 template<int iArySize> void fnTestDataType() { char szBuf[iArySize] = {'\0'}; unsigned short wT

程序员突围-程序调试分析(序)

-从实践到思考,痛苦的煎熬 其实算算,工作一年了,从大学毕业至今,接触编程已经五年了,但是真正的编程感觉还没有开始,从大一开始接触C语言,陆续接触c++,java,C#等等,现在感悟到了一点,编程语言学那么多有什么用呢?其实把一门编程语言学精了,学透了,其他的是触类旁通的(底层的C语言和C++可能有点例外),下面我会说一下我的经历,我感觉可能是大多数学习编程人的必经的阶段,让大家对编程的抵触少一些,然后想想一个我这样的白痴都能慢慢的开始程序调试,程序分析,你们绝对比我强的,下篇文章才会进入我的程

又是正版!Win下ffmpeg源码调试分析二(Step into ffmpeg from Opencv for bugs in debug mode with MSVC)

最近工作忙一直没时间写,但是看看网络上这方面的资源确实少,很多都是linux的(我更爱unix,哈哈),而且很多是直接引入上一篇文章的编译结果来做的.对于使用opencv但是又老是被ffmpeg库坑害的朋友们,可能又爱又恨,毕竟用它处理和分析视频是第一选择,不仅是因为俩者配合使用方便,而且ffmpeg几乎囊括了我所知道的所有解编码器,但是正是因为这个导致了一些bug很难定位,所以有必要考虑一下如何快速定位你的ffmpeg bug. sorry,废话多了.首先给个思路: 1.使opencv 的hi

白屏调试分析

前些天在展讯8825上调试一个ili9807的屏,屏幕一直显示为白屏,这就非常麻烦了,因为白屏意味着基本的显示也没有, 调试屏的时候,即使是显示花屏或者任何的乱彩色线条,甚至是简单的几条线,也算是成功了一小部分,能显示数据, 意味着基本的数据链路是联通的. 开始是从屏的连接器上找问题,从硬件上测量FPC是正常连接的,简单的就是测量屏与主板的各个引脚是连接通的, 各引脚上的电压是正常的,各引脚是否短路,经过长时间的测量和ic原厂的工程师帮助下,硬件在FPC上飞线, 屏的FPC终于是正常联通了!这立

内存分配方式和调试机制

内存分配方式和调试机制 M内存分配 内存分配函数 MFCWin32或者C语言的内存分配API,有四种内存分配API可供使用. Win32的堆分配函数 每一个进程都可以使用堆分配函数创建一个私有的堆──调用进程地址空间的一个或者多个页面.DLL创建的私有堆必定在调用DLL的进程的地址空间内,只能被调用进程访问. HeapCreate用来创建堆:HeapAlloc用来从堆中分配一定数量的空间,HeapAlloc分配的内存是不能移动的:HeapSize可以确定从堆中分配的空间的大小:HeapFree用