By Fabrice Marguerie
尽管很多人相信在.net应用中谈及内存及资源泄露是件很轻松的事情。但GC(垃圾回收器)并不是魔法师,并不能把你完全从小心翼翼处理内存与资源损耗中解放出来。
本文中我将解释缘何内存泄露依然存在以及如何避免其出现。别担心,本文不涉及GC内部工作机制及其它.net的资源及内存管理等高级特性中。
理解泄露本身及如何避免其出现很重要,尤其因为它无法轻松地自动检测到。单元测试在此方面无能为力。一旦产品中你的程序崩溃了,你需要马上找出解决方案。所以在一切都还不是太晚前,花些时间来学习一下本文吧。
Table of Content
· 介绍
· 泄露?资源?指什么?
· 如何检测泄露并找到泄露的资源
· 常见内存泄露原因
· 常见内存泄露原因演示
· 如何避免泄露
· 相关工具
· 结论
· 资源
介绍
近期,我参与了一个大的.net项目(暂叫它项目X吧),我在项目中负责追踪内存与资源泄露。大部分时间我都花在与GUI关联的泄露上,更准确地说是一个基于Composite UI Application Block (CAB).的windows窗体应用。接下来我要说的直接应用到winform上的内容,多数见解同样可以适用到其它.net应用中(像WPF,Silverlight,ASP.NET,Windows service,console application 等等)。
我不是个处理泄露方面的专家,所以我不得不深入钻研了一下应用程序,做一些清理工作。本文的目标是与你们分享在我解决问题过程中的所得所悟。希望能够帮助那些需要检测与解决内存、资源泄露问题的朋友。下面的概述部分首先会介绍什么是泄露,之后会看看如何检测到泄露和被泄露资源,以及如何解决与避免类似泄露,最后我会列出一个对此过程有帮助的工具列表及相关资源。
泄露?资源?指什么?
内存泄露
在进一步深入前,让我们先来定义下我所谓的“内存泄露”。简单引用在Wikipedia上找到的定义吧。该定义与我打算通过本文所帮助解决的问题完美的一致:
在计算机科学领域中,内存泄露是指一种特定的内存损耗,该损耗是由一个计算机程序未成功释放不需要的内存引起的。通常是程序中的BUG阻碍了不需要内存的释放。
仍然来自Wikipedia:”以下语言提供了自动的内存管理,但并不能避免内存泄露。像 Java,C#,VB.NET或是LISP等。”
GC只回收那些不再使用的内存。而使用中的内存无法释放。在.net中,只要有一个引用指向的对象均不会被GC所释放。
句柄与资源
内存可不是唯一被视为资源的。当你的.net应用程序在Windows上运行时,消耗着一个完整的系统资源集。微软定义了系统三类对象:用户(user),图形设备接口(GUI),以及系统内核(kernel)。我不会在此给出完整的分类对象列表,只是指出一些重要的:
· 系统通过使用用户对象(User objects) 来支持windows管理。相关对象包括:提速缓冲表(Accelerator tables),Carets(补字号?),指针(Cursors),钩子(Hooks),图标(Icons),菜单(Menus)和窗体(Windows)。
· GDI对象 支持图形绘制:位图(bitmaps),笔刷(Brushes),设备上下文(DC),字体(Fonts),内存设置上下文(Memory DCs),元文件(Metafiles),画笔(Pens),区域(Regions)等。
· 内核对象 支持内存管理,进程执行和进程间通讯(IPC):文件,进程,线程,信号(Semaphores),定时器(Timer),访问记号(Access tokens),套接字(Sockets)等。
所有系统对象的详细情况都可以在MSDN中找到。
系统对象之外,你还会碰到句柄(handles).据MSDN的陈述,应用程序不能直接访问对象数据或是对象所代表的系统资源。取而代之,应用程序一定都会获得一个对象句柄(Handle),可以使用它检查或是修改系统资源。在.net中无论如何,多数情况下系统资源的使用都是透明的,因为系统对象与句柄都由.net类直接或间接代表了。
非托管资源
像系统对象(System objects)这样的资源自身都不是个问题,但本文仍涵盖了它们,因为像Windows这样的操作系统对可同时打开的 套接字、文件等的数量都有限制。所以关注应用程序所使用系统对象的数量非常重要。
在特定时间段内一个进程所能使用的User与GDI对象数目也是有配额的。缺省值是10000个GDI对象和10000个User对象。如果想知道本机的相关设置值,可以使用如下的注册表键:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows: GDIProcessHandleQuota 和 USERProcessHandleQuota.
猜到了什么?确实没有这么简单,还有一些你会很快达到的其它限制。比如参照:我的一篇有关桌面堆的博客 所述。
假设这些值是可以自定义的,你也许认为一个解决方案就是打破默认值的限制—调高这些配额。但我认为这可不是个好主意,有如下原因:
1. 配额存在的原因:系统中不是只有你独自一个应用程序,所有运行在计算机中的其它进程与你的应用应该分享系统资源。
2. 如果你修改配额,使它不同于其它系统了。你不得不确认所有你的应用程序需要运行的机器都完成了这样的修改,而且这样的修改从系统管理员的角度来说是否会有问题也需要确认。
3. 大部分都采用了默认配额值。如果你发现配置值对你应用程序来说不够,那你可能确实有些清理工作要做了。
如何检测泄露及找到泄露的资源
泄露带来的实际问题在MSDN上的一篇文章中有着很好的描述:
哪怕在小的泄露只要它反复出现也会拖垮系统。
这与水的泄露异曲同工。一滴水的落下不是什么大问题。但是一滴一滴如此反复的泄露也会变为一个大问题。
像我稍后解释的,一个无意义的对象可以在内存中维持一整图的重量级对象。
仍然是同一篇文章,你会了解到:
通常三步根除泄露:
1.发现泄露
2.找到被泄露的资源
3.决定在源码中何时何处释放该资源
最直接“发现”泄露的方式是遭受泄露引发的问题
你或许没有见过内存不足。“内存不足”提示信息极少出现。因为操作系统运行中实际内存(RAM)不足时,它会使用硬盘空间来扩展内存。(称为虚拟内存)。
在你的图形应用程序中可能更多出现的是“句柄不足”的异常。准确的异常不是System.ComponentModel.Win32Exception 就是 System.OutOfMemoryException 均包含如下信息:”创建窗体句柄错误”。这两个异常多发于两个资源被同时使用的情况下,通常都因为该释放的对象没有被释放所致。
另外一种你会经常碰到的情况是你的应用程序或是整个系统变更得越来越慢。这种情况的发生是因为你的系统资源即将耗尽。
我来做个生硬的推断:大多数应用程序的泄露在多数时间里都不是个问题,因为由泄露导致出现的问题只在你的应用程序集中使用很长时间的情况下才会出现。
如果你怀疑有些对象在应该被释放后仍逗留在内存中,那需要做的第一件事就是找出这些对象都是什么。
这看起来很明显,但是找起来却不是这样。
建议通过内存工具找到非预期逗留在内存中的高级别对象或是根容器。在项目x中,这些对象可能是类似LayoutView实例一样的对象们(我们使用了MVP(Model View Presentation )模式)。在你的实际项目中,它可能依赖于你的根对象是什么。
下一步就是找出它们该消失却还在的原因。这才是调试器与工具能真正帮忙的。它们可以显示出这些对象是如何链接在一起的。通过查看那些指向“僵尸对象”(the zombie object)的引用你就可以找到引起问题的根本原因了。
你可以选择 ninja方式(译者:间谍方式?)(参照 工具介绍章节中有关 SOS.dll 和 WinDbg 的部分)。
我在项目X中用了JetBrains的dotTrace,本文中我将继续使用它来介绍。在后面的工具相关章节中我会向你更多的介绍该工具。
你的目标是找到最终引起问题的那个引用。不要停留在你找到的第一个目标上,但是也要问问自己为什么这个家伙还在内存中。
常见内存泄露的原因
上面提到的泄露情况在.net中较常见。好消息是造成这些泄露的原因并不多。这意味着当你尝试解决一个泄露问题时,不需要在大量可能的原因间搜寻。
我们来回顾一下这些常见的罪魁祸首,我把它们区别开来:
· 静态引用
· 未注销的事件绑定
· 未注销的静态事件绑定
· 未调用Dispose方法
· Dispose方法未正常完成
除了上列典型的原因外,还有些其它情况也可能引发泄露:
· Windows Forms:绑定源滥用
· CAB:未移除对工作项的调用
我只列出了可能在你应用程序中出现的一些原因,但应该清楚你的应用程序依赖的其它.net代码、库实际使用中也可能引发泄露。
我们来举个例子。在项目x中,使用了一套第三方控件来构造界面。其中一个用来显示所有工具栏的控件,它管理着一个工具栏列表。这种方式没什么,但有一点,即使被管理的工具栏自身实现了IDisposable接口,管理类却永远也不会去调用它的Dispose方法。这是一个bug.幸运的是这发生在一个很容易发现的工作区:只能我们自身来调用所有工具样的Dispose方法了。不幸的是这还不够,工具栏类自身问题也不少:它并没有释放自身承载的控件(按钮,标签等等)。所以在解决方案中还要添加对每个工具栏中控件的释放,但是这次可就没那么简单了,因为工具栏中的每个子控件都不同。不管怎么样这只是一个特殊的例子,我要表达的观点是你应用程序中使用的任何第三方库、组件都可能引发泄漏。
最后,还有一种由.net framework造成的泄露,由一些不好的使用习惯引起。即使.net framework自身可能引发泄露,但这是你极少会遭遇到的情况。把责任推到.net身上很容易,但在我们把问题推到别人头上前,还是应该先从自身写的代码出发,看看里面有没有问题。
常见泄露演示
我已经列举出了泄露主要的来源,但我还不想仅限于此。如果每个泄露我都能举个鲜活的例子的话,我想本文会更实用些。好,我们先启动Vs 和 dotTrace , 然后看些示例代码。我会同时演示如何解决或是避免每个泄露情况。
项目X中使用了CAB和MVP模式,这意味着界面由工作空间、视图和呈现者组成。简单起见,我决定使用包含一组窗口的Winform应用。其中使用了与Jossef Goldberg的一篇关于“Wpf应用程序内存泄露”文章中相同的方法。甚至我会直接把相同的例子和事件处理函数应用到我的Winform App中。
当一个窗体被关闭及处置后,我们期待的结果是它同时也在内存中被释放了。对吧?但我下面要展示的是何种情况下该窗体未被释放。
下面的就是我创建的示例程序中的主窗口:
这个主窗口可以打开不同的子窗口;每个打开的子窗口都会分别引发不同的内存泄露。
本文相关示例代码在后面的“资源”一节中可以找到。
静态引用
我们先把显而易见的放在一边。如果一个对象被一个静态字段引用,那它永远也不会被释放。
像singletons模式中就是如此。每个Singletone对象通常都是一个静态对象,即使不是静态对象,那它至少也是会长期存在的对象。
这种情况很显而易见,但是记住不只是直接引用才危险。真正的危险往往来自间接引用。事实上,你一定要注意引用链。整个引用链中有多少个根。如果有静态对象作为根,那所有它的子节点将会始终存在着。
上图上如果Object1是静态的话,多数情况下会长时间存在,那所有它下面的引用将会一直在内存中保留着。危险就在于链条太长了以至于忽略了根节点是静态的。如果你只关注在一个深度级别上,考虑Object3和Object4一旦Object2离开内存那它们也就被释放了。这个没错,确定是,但你需要考虑到它们可能因为Object1一直存在而未被释放。
小心各种静态类型。如果可能尽量不用。非要使用的话,花些时间关注它们。
一个来自特定类型的风险—静态事件,我会在讲解常规事件相关内容时介绍它。
事件,或 “失效监听器”问题
一个子窗口订阅了主窗口中的一个事件,以便主窗口透明度变化时得到通知(包含在EventForm.cs文件中):
C#
mainForm.OpacityChanged += mainForm_OpacityChanged;
问题就是这个针对OpacityChanged事件的订阅创建了一个从主窗口到子窗口的引用。
下图显示了完成事件订阅后两个对象间是如何通讯的:
看看我这篇更多学习事件与引用的博文。下图就是该文章中体现事件观察与被观察者背后引用关系的:
下图所示就是你使用dotTrace搜索EventForm然后点击“最短路径”的结果:
应能看到主窗体(MainForm)保留着对事件窗体(EventForm)的引用。这种情况会出现了第一个你在应用中打开的事件窗口(EventForm)。这就意味着所有在应用中打开的事件窗口(EventForm)只要程序还未销毁,哪怕你已经不在使用它们了(包括关闭了它们)。
这些子窗口不光只是赖在内存中,它们还可能会引发异常,比如你改变了主窗口(MainForm)的透明度(opacity),那些已经被关闭的事件窗口(EventForm)就会引发异常,因为主窗口依然通过事件通知他们,但它们已经是“已处置(disposed)窗口”了。
最简单的解决方案就是通过在这些事件窗口(EventForm)被处置(dispose)时取消事件订阅,从而移除主窗口对事件窗口的引用:
C#
Disposed += delegate { mainForm.OpacityChanged -= mainForm_OpacityChanged; };
注意:我们这里有一个问题,MainForm对象在整个应用被关闭后依然在内存中存在。较短的生命周期内的对象相互引用可能不会引起内存问题。任何孤立的对象链都会被GC自动从内存中卸载掉。孤立的对象链由两个单向引用的对象或是一组没有外部引用的连接对象组成。
另一个解决方案是使用基于弱引用的弱委托。在我的那篇《事件与引用》的博文中有涉猎。网上也有几篇文章讲解了如何付诸实现。比如这篇:“弱引用事件”。找到的多数解决方案都是基于弱引用类。更多弱引用方面的学习可以参见MSDN。
注意一下,一个以“弱事件模式”形成的解决方案已经在WPF中存在了。
现有的一些框架中如:CAB (Composite UI Application Block) 或Prism (Composite Application Library) 均有一些其它的解决方案,像EventBroker 和EventAggregator 。只要你想也可以使用自己实现的其它事件模式:broker/aggregator/mediator。
订阅了静态对象或是生命周期较长的对象上的事件却没有适时取消订阅也会造成问题。另外一种问题来源于静态事件。
静态事件
来直接看个例子(StaticEventForm.cs中):
C#
SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
这次的例子与前面的很相似,不同的是这次我们订阅的是一个静态事件。因为是一个静态事件,所以对应的监听对象永远也不会被释放掉。
解决方案就是当我们的监听者完成所要做的事情后取消该静态事件订阅。
C#
SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged;
Dispose方法没有被调用的情况
你是否已经开始注意事件和静态类型了呢?很好,但这还不够。你仍能发现一些游弋的未释放对象,即使你写了正确的清理代码。这种情况的发生有时仅仅只是因为这些清理的代码未被调用。。。
在Dispose方法或是Disposed事件中取消事件订阅和释放资源是很好的习惯,但如果未调用Dispose那也没有用。
再来看个有趣的例子。下面示例代码给一个窗体创建了一个上下文菜单(来自ContextMenuStripNotOKForm.cs):
C#
ContextMenuStrip menu = new ContextMenuStrip(); menu.Items.Add("Item 1"); menu.Items.Add("Item 2"); this.ContextMenuStrip = menu;
在窗口已经关闭且Dispose的情况下,下图是你可以通过dotTrace 看到的结果:
ContextMenuStrip仍在内存中!注意:想复现该问题,先通过右键鼠标显示上下文菜单,然后关闭窗口。
这就是一个由静态事件引发的泄露。通常解决方法就是下面的在Disposed事件处理程序中调用ContextMenuStrip的Dispose方法。
C#
Disposed += delegate { ContextMenuStrip.Dispose(); };
我猜你已经开始明白如果不多加小心,在.net中使用事件是很危险的。此处我想强调的是只此一行代码就轻易的引起了泄露。而当你创建一个Context-menu时是否考虑过潜在的内存泄露风险呢?
可能比你想象的还要糟。不只ContextMenuStrip未被释放,它使得整个窗体都还在内存中保留着!在下面截图中你可以看到ContextMenuStrip引用着窗体:
导致的结果就是只要ContextMenuStrip还在内存中,整个窗体也会被释放。哦,当然你也不要忘了,因为窗体还在,与其相关的一组对象都会持续保留在内存中 – 像窗体上的控件包含的组件,如下图所示:
这是我严重警告一定要对这种情况足够注视的原因。可能因为一个小对象而使内存中一个大的对象链不能被正常释放。我总能在项目X中看到这种情况的出现。水滴石穿,不要小看一滴水能带来的破坏。
因为单个控件不是指定它的父控件就是指向父控件的事件,所以这使得如果某个控件没有被调用dispose而致整个对象链都无法释放的潜在可能。这当然也包括上级容器包含的其它控件。所以示例中导致整个窗体还问题存在于内存中的情况也就可能出现(至少在整个应用程序完全终止前是这样的)。
在这个具体例子中,你可能正在考虑是否有ContextMenuStrip的地方总是出现这个问题呢。当然不会问题出现,使用设计器直接在窗体上直接创建,至少在此情形下,Vs自动生成的代码可以确保ContextMenuStrip相关组件可能被正确处置。
如果你对于设计器是如何处理的好奇,可以看看ContextMenuStripOKForm类及它的字段在ContextMenuStripOKForm.Designer.cs中是如何处理的。
我想指出另外一个在项目x 中看到的解决方法。由于某些原因,有些控件在源文件中没有与之相关的.Designer.cs文件。设计器代码生成的代码就在.cs文件中。别问我为什么。除了与众不同的代码结构(不推荐)外,问题在于这些代码是被完整拷贝过去的:但要吗Dispose方法就没有,要吗就是没有调用组件的Dispose方法。我想你能明白这种情况下出问题就不奇怪了。
不完整的Dispose方法
我想现在你已经领会了调用那些实现Dispose模式类的Dispose方法的重要性了,强调Dispose这是我要说的一件事情。在你的类中实现IDispose接口以及包含其它Dispose的调用这非常棒,对你的程序也非常有益,只要Dispose方法实现正确。
这段评论看起来好像有点儿傻,但是我这么做是因为我看到太多这种不完整实现Dispose的例子了。
你清楚它是怎么发生的。你创建好了自己的类;实现了IDisposable接口;你在Dispose方法中取消事件订阅和释放资源;并且你在所有需要调用Dispose的地方都调用了它。这很好,直到后来你的类中有一个订阅了新的事件或是消耗了新的资源。编码很容易,你热情洋溢的完成编码、测试。它运行起来很好,所以你很高兴。你提交代码,不错!但是。。。唔,你忘记了更新Dispose方法去释放新的事件或资源了。这种事情总在发生。
我就不举例了。这应该相当常见。
Windows窗体:绑定源误用
我们来解决一个Windows窗体上的问题。如果你用BindingSource组件,请确认你是按照它设计时给定的方式使用的。
通过静态引用,我已经看到了出现的BindingSource,而BindingSource的运转方式导致了内存泄露。使用BindingSource作为数据源的控件都保留着一个BindingSource的引用 ,即使这些控件被处置后。(Disposed)
下图所示的情况,就是在一个数据源是静态或长生命周期的BindingSource(如:BindingSourceForm.cs)的ComboBox被处置后 用doTrace查看的结果。
一个解决方案就是用一个BindlingList替代BindingSource.如你可以这样,拖放一个BindingSource到你的窗体上(指设计时),将BindingList分配给BindingSource作为它的数据源,同时把BindingSource分配给ComboBox作为它的数据源。这种方式下,你仍将使用一个BindingSource。
看BindingListForm.cs(例子源码中)中的这个处理。
这种方式并没有妨碍你使用BindingSource,但是应该在视图界面中创建它 (设计时窗体中,从而生成自动化代码。).总之这样做是有道理的:BindingSource是一个定义在System.WindowsForms命名空间中的表现层组件。BindingList比较而言,它只是一个集合,不隶属于可视化的组件。
注意:如果你并不是非用BindingSource不可,可以完全只用BindingList。
CAB:缺少从工作项(WorkItem)上移除
下面是一条对针对CAB应用程序的建议,但是你也可以应用到其它类型的应用中。
工作项(WorkItems)是构建CAB应用程序的中心。一个工作项(WorkItems)就是一个在上下文中保留相关对象轨迹的容器,并且执行依赖注入。通常一个视图(View)创建后就会增加一个工作项(WorkItems)与之对应。当视图关闭且被回收后,它应该从对应的工作项(WorkItems)上移除,否则工作项(WorkItems)就会使得它(视图)始终存于内存中,因为工作项(WorkItems)中维护着一个指向视图的引用。
如果你忘记了从对应工作项(WorkItems)上移除视图,那泄露由此产生。
在项目x中,我们使用了MVP设计模式(Model-View-Presenter)。下图显示了一个视图显示后不同元素间是如何连接的:
(译者:此处的插图有误,所以未添加)
注意WorkItem通过依赖注入得到presenter。而WorkItem多数时候又会顺便把presenter注入到视图(View)中。为了确保项目X中的所有内容都能适当的释放掉,我们使用了下图所示的一个职责链:
当一个视图(View)被处置(disposed)了(很可能就是因为它被关闭了),那它的Dispose方法就会被调用。该方法会依次调用presenter的dispose方法。Presenter认识WorkItem,在它的Dispose方法中会把自己和源头的视图从Worktem中移除掉。通过这种方式,所有内容都被适当的处置和释放了。
我们的应用程序框架中包括了实现了上面职责链条的基类,所以视图开发人员不需要重要实现这些类也不用每次都为此担心。即使不是CAB应用程序,我也鼓励在你的应用中实现这类模式。在你的对象中正确实现自动释放模式将助你避免那些由于疏忽引起的泄露。但要确保所有实现的方式一致,不要因为他们不知道适当的处理方式而出现每个开发人员实现都不同的情况,这也会导致泄露。
如何避免泄露
现在你对泄露本身以及它是如何产生的有了进一步的了解,此时我想强调几个重点并给出几个技巧。
我们先来探讨一条一般的规则。通常一个负责创建另外一个对象的对象也负责处置(disposing)它。当然不包括工厂类。
反过来:一个对象对于从别的对象上得到的对象没有处置(disposing)的职责。
事实上,这确实要依靠具体情形而定。无论如何,重要的是谨记对象属于谁(who created it)。
第二条原则:每一个+=(事件订阅)都是一个潜在的敌人!
根据我的个人经验,事件是.net中的主要泄露来源。它值得你反复确认甚至确认再三。每次你在代码中增加事件订阅,都应该考虑一下结果问问自己是否需要再增加一个-=来取消事件订阅。如果答案是需要,在你没忘记前马上加上。经常是加到Dispose方法中。
为了确保对象被成功回收,推荐的做法是有事件订阅的对象就需要对应的事件取消订阅。无论如何,当你绝对清楚一个事件源将不再发布事件通知了,而且你希望所有订阅它事件的其它对象能被释放,那么你可以强制移除所有该事件的订阅。我一篇博文中包含了如何做的代码事例。
马上给出一个技巧。通常当对象引用在一定数量的多个对象间共享时,问题就会出现了。因为这种情况下明了哪个对象引用了哪些引用就很困难了。有时在内存中克隆所需的对象要胜于引用现有对象,这样可以避免对象间反复缠绕。
最后,即使这已是.net中众所周知了,我还是想要再次强调调用Dispose的重要性。每次你分配了一个资源,一定要确保调用了Dispose或是将资源使用的代码写在一个using代码块中。如果你不是始终都这么做的话,你很快会被资源泄露搞死,通常情况下都是非托管资源引起的。
Tools
相关工具
几款工具可能助你追踪对象实例、也包括系统对象和句柄。我来列举几个。
Bear
Bear是一个可显示出所有Windows下运行进程信息的免费程序:
· 支持所有GDI对象的用法(hDC, hRegion, hBitmap, hPalette, hFont, hBrush)
· 支持所有用户对象的使用(hWnd,hMenu,hCursor,SetWindowsHookEx,SetTimer 和其它形式的对象)
· 句柄统计
GDIUsage
另外一个实用的工具是GDIUsage.这款工具也是免费的而且开源。
GDIUsage聚集在GDI对象上。通过它,你可以对当前GDI消耗情况拍摄快照,执行一个可能诱发泄露的行为,之后比较泄露前后资源的使用情况。这样可以大大的帮助我们,它能让我们看到操作期间增加了(或是释放了)哪些GDI对象。
此外,GDIUsage不光只是给出一个数字,还可以提供GDI对象的图形化显示。肉眼观察位图(bitmap)泄露的内容可以轻松的找出泄露的原因。
dotTrace
JetBrains出品的dotTrace是一个.net程序的内存与性能分析工具。
下图就是dotTrace的截屏。这也是项目X中我用的最多的工具。其它.net分析工具我也不太了解,但是dotTrace为我解决项目X中检测到的泄露提供了所需的信息。多达20个以上。。。我没说过这就是一个bug项目?
dotTrace允许你及时标识出特定时间下内存中的对象,它们如何存在(被哪些对象引用),以及它们是谁(谁被引用)。你还可以使用它提供的高级调试功能:追踪栈分配情况,查看销毁对象列表等。
下图展示的是两种内存状态间的差别
dotTrace也是一个性能分析工具:
使用的方法就是先启动dotTrace然后指定exe文件的路径,之后就可以请求它对你选择的应用程序进行分析了。
如果你想检查应用程序的内存使用情况,可以在程序运行时用dotTrace拍摄快照,然后让它显示出相应信息。你要做的第一件事大概就是让它显示出指定类在内存中有多少个实例,以及这些实例是如何存在的。
除了搜索托管实例外,你也可以搜索非托管资源。dotTrace没有对非托管资源追踪提供直接支持,但你可能搜索对应的.net包装对象。例如:你搜索位图、字体或是笔刷类的实例。如果你发现一个实例没有被释放,那么它在应用程序上分配的资源也仍然还在。
下一个我要介绍的工具就内置了对非托管资源的追踪。也就是说通过它你就能够直接探索HBITMAP, HFONT 或是 HBRUSH 句柄。
.net内存分析器
.net内存分析器是另一个有趣的工具。它提供了一些dotTrace不包括的实用特性:
· 查看哪些已经调用了处置方法,却还存在的对象
· 查看哪些已经被释放却没有调用处置方法的对象
· 非托管资源的追踪
· 附加到一个运行中的进程上
· 附加到一个进程的同时作为VS的调试器
· 自动内存分析(关于常见内存使用问题的提示和警告)
另外一些可用的内存分析器
上述几个工具只是一些工具所能帮助你的示例。dotTrace和.NET Memory Profiler是众多.net内存及性能分析工具中的两个。其它一些有名的包括:ANTS Profiler,YourKit Profiler,PurifyPlus, AQtime 和 CLR Profiler.这些工具中的多数都提供了和dotTrace相同类型的功能。在SharpToolbox.com上你可以找到完整的专注于.net的分析工具集合。
SOS.dll and WinDbg
另一个你可用的工具是SOS.dll. SOS.dll 是一个扩展调试的工具,帮助你在WinDbg.exe调试器和VS中调试托管程序,提供CLR资源有关的内部信息。可以用它来获取与GC相关的信息,与内存中对象、线程与锁、调用栈等相关的信息。
WinDbg 是你通常需要附加到产品中某个进程时用的较多的工具。关于SOS.dll 和 WinDBg 如果想更多了解可以看看 Rico Marian 的一篇博文,和Mike Taulty的两篇博文(SOS.dll 与 WinDbg 和 SOS.dll 与 Visual Studio) ,还可以参照Wikipedia。
SOS.dll 和 WinDbg 作为Windows调试工具包中的一部分由微软免费提供。二者比之上述的其它工具的一大优势就是在保持强大功能的同时又有着较低的资源损耗。
下图是使用sos.dll和gcroot命令的示例输出:
WinDbg screenshot:
自定义工具
除去市面上的可用工具外,别忘了你也可以创建自己的工具。可以在多个应用程序中重用的独立工具。当然开发这种工具可能有点儿难度。
我们为项目X开发了一套完整工具,帮助我们保持实时的资源使用情况和潜在泄露情况进行跟踪。
这些工具之一在右侧显示一组存在与消亡的对象列表。它由一个CAB服务和一个CAB视图组成,可以用来检查我们期望被释放的对象是否真的被释放了。
下图就是该工具的截图:
如果保留应用中所有对象的痕迹,对于大的应用程序来说代价太高并且也违背产品设定的对象数量。事实上,我们无需关注所有的对象,只要关注那些应用中的高级别对象和根容器。这些对象我在解释如何检测泄露时已经提议过进行追踪。
创建该 工具使用的技术很简单。它使用了弱引用。弱引用类允许你引用一个同时可被GC回收的对象。此外,它还通过提供IsAlive属性提供你测试该引用是否已消亡的功能。
项目X中,我们还有一个提供GDI和用户对象使用概况的小部件。
当资源接近枯竭之时,这个小部件会有一个小的警告图标:
此外,该工具还会让用户关闭一些当前打开的窗口/选项卡/文档,并且阻止用户打开新的窗口等,直到资源使用情况重新低于临界级别。
为了读取到当前UI资源使用情况,我们使用了User32.dll中的GetGuiResourcesAPI。下面代码展示了如何在C#中引入该API:
C#
// uiFlags: 0 - Count of GDI objects
// uiFlags: 1 - Count of USER objects
// GDI objects: pens, brushes, fonts, palettes, regions, device contexts, bitmaps, etc.
// USER objects: accelerator tables, cursors, icons, menus, windows, etc.
[DllImport("User32")]
extern public static int GetGuiResources(IntPtr hProcess, int uiFlags);
public static int GetGuiResourcesGDICount(Process process)
{
return GetGuiResources(process.Handle, 0);
}
public static int GetGuiResourcesUserCount(Process process)
{
return GetGuiResources(process.Handle, 1);
}
通过Process.GetCurrentProcess()的WorkingSet64属性,获取内存使用情况。
结论
我希望本文为你改善应用程序和解决泄露方面提供了一个良好的基础。追踪泄露可以很有趣。。。如果你确实没什么其它更好的事情做的话:-)有时,你别无选择,因为对于你的应用程序来说解决泄露至关重要。
一旦解决了泄露,仍有工作要做。我强烈建议你在尽量降低资源消耗方面改善应用程序。不要损失功能。最后,我邀请你阅读我的另外一篇包含相关建议的博文。
相关资源
演示程序源代码下载地址。
假如你有意进一步钻研,下面是一些有趣的补充资源
· Jossef Goldberg: Finding memory leaks in WPF applications
· Tess Ferrandez has a series of posts about memory issues (ASP.NET, WinDbg, and more)
· MSDN article by Christophe Nasarre: Resource Leaks: Detecting, Locating, and Repairing Your Leaky GDI Code
· Article in French by Sami Jaber: Audit et analyse de fuites mémoire
· My blog post about the Desktop Heap
· My blog post about lapsed listeners
· My blog post that shows how to force unsubscription from an event