如何在.net应用中发现和避免内存和资源泄露

全部回答1
默认 最新
  • 尽管很多人相信在.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中。
    0 点赞

没有更多内容了

返回顶部
产品求购 求购