使用WinDbg查找内存泄漏问题

WinDbg安装

下载地址:32位 64位

clip_image001

双击运行,在Install Options中选择“Common Utilities”下的“Debugging Tools For Windows”:

clip_image003

(我这里已经安装过了,所以显示是灰色的。)

安装完成之后,可以在“开始”-“所有程序”-“ Debugging Tools for Windows (x86)”中找到。

将WinDbg安装目录( “C:\Program Files\Debugging Tools for Windows (x86)”)添加到“Path”环境变量中。

场景设计

比如有一个多文档应用程序,在代码中设计了一个管理类用于管理打开的各个文档窗口,为了便于各个模块通过访问这个类获取打开的文档清单,因此将类中的List定义为静态变量。但是由于程序员的疏忽,在文档关闭的时候List中对应的引用并没有删除,导致内存泄漏。

场景创建

Visual Studio中创建一个WindowsApplication,新建一个MDIParent窗口:

clip_image005

修改Program.cs中的启动窗口为此MDIParent。添加一个名为“DocumentEditorManager”的类,用下面的代码替换里面的内容:

在MDIParent.cs文件中查找到ShowNewForm方法,修改为如下代码:

 

按F5运行程序之后,打开任务管理器,多次点击工具栏中的“New”按钮,发现内存一直在上涨,即使把打开的窗口关闭了,内存依旧未释放。

发现问题原因

关闭程序,在工程下找到生成的EXE文件并运行(不要直接在VS中运行程序)。打开WinDbg,在菜单中选择“Attach to a Process”:

clip_image006

在打开的窗口中找到对应的程序,点击OK:

clip_image007

这时候,程序已经不能交互了,因为已经进入调试模式,类似于VS中命中断点时的情景。在WinDbg中能看到程序所加载的所有文件:

clip_image009

WinDbg支持扩展DLL来提供更加丰富的调试功能,调试.NET程序需要用到的是“SOS.DLL”模块(SOS.DLL所支持的所有命令可以在这里找到。),它包含在每个版本的.NET Framework中(比如2.0版本的路径为C:\Windows\Microsoft.NET\Framework\v2.0.50727\SOS.dll),安装完WinDbg之后已经有一份在WinDbg的安装目录下了,输入下面的命令加载“SOS.DLL”:

在WinDbg中按F5继续运行程序,使其跳出调试模式,切换到程序中,按10次“New”按钮创建10个窗口,然后关闭这10个窗口(这个过程相当于程序运行了一段时间回到最初的状态,但是内存没有减少),我们需要通过WinDbg找到是什么类出现了问题。切换到WinDbg,在菜单“Debug”中找到“Break”点击中断程序:

clip_image010

输入“!dumpheap –type WindowsFormsApplication3”罗列出我们程序的所有类实例:

clip_image012

MT与类型的概念差不多,只要类型一样,这个MT地址就一样,红色的方框中可以看到有两种,一种是1个,另外一种是10个,与Statistics中的内容一致。

理论上来说,所有的Form1都已经关闭了,应该不存在这样的对象,有些人可能觉得是垃圾回收器还没来得及回收的缘故,我们可以做一个实验,修改程序,在工具栏中加一个按钮“Clear Memory”,并在Click事件中编写如下的代码:

当关闭10个窗口之后点击此按钮,再在WinDbg中用dumpheap命令查看对象清单,发现它们依旧存在。

这说明垃圾回收器并没有回收它们,.NET中的垃圾回收器只会回收没有被使用的对象,所以这些Form1对象依旧被使用,那是谁在使用它们呢,我们可以通过查看其中一个对象实例的引用列表来找到源头:

clip_image014

比如看其中一个地址为“01f78160”的对象引用,使用下面的命令:

(注意每台机器上地址不同)下面长的方框中是某一个引用的路径,箭头最终指向是Form1。后面绿色和粉色的根是一个Weak引用,这个对垃圾回收器没有影响。而红色方框的根为“HANDLE(Pinned)”,这表示一个正在使用的对象,后面看到类型是System.Object[],可能是个数组,也可能是静态变量,因为.NET运行时中所有静态变量都是用Object[]存储的,它的第二级是一个List<Form>类型的,从这里就能找到问题的原因了。

解决问题

我们修改Form1的代码,添加窗口的Closing事件处理函数,修改为以下的代码:

这样在Form1关闭的时候就会从List中移除,这时候再重复上面的步骤,可以看到在使用dumpheap命令的时候已经与前面的情况不同了:

clip_image016

从图中,我们可以看到,Form1还有一个对象存在,这是.NET内部机制引起的,每个MDI的窗口都有一个PropertyStore,用于优化性能,只有最后一个关闭的子对话框会被引用,所以它不会导致内存一直增长,这问题本来在.NET 2.0的时候没有的,但是.NET 2.0 SP1的时候为了修复另外一个问题导致这个问题出现了,详细的解释可以见这里

总结

SOS.DLL是一个强大的扩展,还有非常多的功能。各个命令也支持很多参数。也可以在VS的调试器中使用SOS.DLL,VS中进入调试模式之后,可以使用相同的命令.load sos.dll来加载SOS.DLL,但是个人感觉用起来不太顺,还是WinDbg中用起来比较顺畅。



Dec03