调试内存泄漏问题的一些经验
内存泄漏(memory leak)是软件中经常遇到的一类问题,这类问题又是比较难以检测的,通常我们在程序遇到Out Of Memory
的异常时才会注意到。拿到Out Of Memory
的dump文件后,如何分析dump文件找到内存泄漏的线索又是一个难点。这篇文章分享了一些在Windows平台如何调试,检测C++和C#的内存泄漏的一些经验。
一、内存泄漏的Dump分析
通常拿到Out Of Memory
的dump之后,用windbg打开,抛出异常的call stack一般是在分配内存时,这个call stack其实意义不大,我们需要知道的是内存为什么被用完了。
一般第一个要运行的命令是!address -summary
,它会给出一个内存情况的总结,如下所示。
第一个部分是使用情况,按照大小排序。通常排第一的不是Heap
就是<unkown>
。Heap
是C++的非托管内存,<unkown>
是C#的托管内存。
第二个部分是类型情况,分了3类,分别是:
MEM_PRIVATE
:当前进程独占的内存。MEM_MAPPED
:映射到文件的内存,这些文件不属于进程程序本身,比如Memory Mapping File。MEM_IMAGE
:映射到进程程序的内存,比如程序加载的dll。
最后一个部分是最大连续内存,比如上图中我们可以看到现在最大的连续可用内存只有500k了。
非托管(C++)内存泄漏分析
如果!address -summary
的输出中发现Heap
被用掉了很多,那很有可能有C++的内存泄漏,我们需要检查堆(heap)来找到可疑的对象。
1.通过!heap -s
来看堆的使用情况,会把堆按照大小列出来,如下所示:
|
|
2.我们用!heap -stat -h <HeapEntry>
来看最大的那个堆,它会列出这个堆上分配的所有对象的统计情况。如果幸运的话我们会看到某个大小的对象数目非常大,占用了很多内存。比如下面的例子中大小为1f64
的对象有0x76c6
个,占了99%的内存。
|
|
3.我们可以用!heap -flt s <ObjSize>
来列出所有大小是制定大小的对象。输出结果中的UserPtr
就是对象的地址,然后可以用d
命令来显示这个地址的内容。如果幸运的话,比如这个地址直接存了个字符串,那就好办多了。
4.还有一些情况我们可能能猜到是某些对象泄漏了。比如如果在!address -summary
的输出里我们看到MEM_MAPPED
大的离谱,而我们程序里所有的MemoryMappingFile都继承自某个基类,那么我们就可以直接看看内存中有多少个这类对象。
5.用命令x modulename!*classname*table*
来找内存中虚表的地址。
6.用命令!heap -srch vtableaddress
来找到所有的对象。
7.用命令dt modulename!classname objectaddress
来看对象的内容是什么,接着就能分析出为什么这些对象有这么多。
托管(C#)内存泄漏分析
如果!address -summary
的输出中发现<unkown>
被用掉了很多,那很有可能有C#的内存泄漏,调试相对简单。
1.运行loadby sos mscorwks
(.net4之前)或者loadby sos clr
(.net4及以后)来加载SOS扩展。
2.运行!dumpheap -stat
来看托管堆的统计信息,输出如下:
total 976456 objects
Statistics:
MT Count TotalSize Class Name
71497de4 1 12 System.Runtime.Remoting.Channels.Tcp.TcpClientTransportSinkProvider
...
输出是按照TotalSize的递增顺序显示的,直接翻到最后一行,看看是哪个对象占用了最大的TotalSize。
3.运行!dumpheap -mt <mt>
来把内存中这个Method Table的所有对象都列出来。结果的第一列就是对象的地址。
4.运行!do <address>
来看这个对象的内容是什么。
5.运行!gcroot <address>
来看这些对象是被谁引用的,这样多半就能找到发生内存泄漏的原因了。
GDI句柄超过限制
还有一种发生Out Of Memory
异常的情况是GDI句柄超过限制了,可以看到dump中crash的call stack中是有关句柄操作的。默认情况下Windows的每个进程的GDI句柄额度是10000,可以通过注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Windows\GDIProcessHandleQuota
来修改这个值。
这种情况相对比较好处理,windows上的GDI对象就这些:Bitmap,Brush,DC,Enhanced metafile,Enhanced-metafile DC,Font,Memory DC,Metafile,Metafile DC,Palette,Pen and extended pen,Region。
二、内存泄漏的实时调试
如果可以非常容易重现的话,可以实时调试内存泄漏,这样就会容易很多了。
非托管内存泄漏检测
使用VLD来检测内存泄漏。
VLD是一个VC++的开源内存泄漏检测工具,非常易于使用。在调试器中运行程序,会在结束时生成一个内存泄漏的报告,包含内存分配的call stack。
打开“Create user mode stack trace database”,分析dump
可以用gflags打开“Create user mode stack trace database”,如下所示,这样就会记录下来每个对象创建的call stack,可以就可以很容易的查到泄漏对象是怎么创建出来的了。
使用Windbg的!heap -l
命令。
- 收集dump,用Windbg打开,然后运行命令
.logopen d:\leak.txt
打开log。 - 运行
!heap -l
命令,会把所有泄漏的对象列出来,附带创建的call stack。可以很容易的写个程序来分析这个输出,合并重复的对象,计算总大小。
使用Windbg的!heap -p -a <address>
命令
按照上面提到的非托管(C++)内存泄漏分析方法来分析dump,最后找到可以的对象时可以直接运行!heap -p -a <address>
命令来看到这个地址的对象的创建call stack。
使用UMDH
UMDH是Windows Debugging Tools里的,和Windbg在同一个目录里,可以用UMDH收集多个内存的log,然后比较,找出泄漏的对象。
托管(C#)内存泄漏检测
Visual Studio 2013加入了调试托管内存的功能,在打开dump文件后可以选择”Debug Managed Memory”,可以看到托管对象的大小,数目,root等信息。
也可打开多个dump通过选择“Select baseline”进行内存比较,可以看到内存的变化。
GDI句柄监测
GDIView是一个免费的小工具,可以监测GDI使用情况。