65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ANTS Profiler 跟踪 .NET 应用程序中的内存泄漏

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2007年2月12日

CPOL

8分钟阅读

viewsIcon

54963

Recognin Technologies 的首席开发人员 Mike Bloise 分享了他在一个使用 C# 构建的近期 CRM 项目中的经历,在该项目中,他面临着严重的内存泄漏问题,并且有一个非常紧张的截止日期。

这是我们CodeProject赞助商的展示评论。这些评论旨在为您提供我们认为对开发人员有用且有价值的产品和服务信息。

引言

在 C 或 C++ 中编码时,内存泄漏相对容易引入(没有人会喜欢为他们所有的 C++ 类编写析构函数)。然而,当你在 .NET 语言(如 C#)中编码时,你是在托管代码中工作,有自动垃圾回收,所以内存管理就成了个小问题,对吧?

这确实 pretty much 描述了我在开发公司销售和 CRM 应用程序的全新 C# 2005 桌面版本时的心态。我对 C# 还不熟悉,虽然我知道如果引用没有得到妥善清理,仍然可能出现问题,但在开发过程中我并没有太多考虑内存管理。我肯定没料到这会是个大问题。结果证明,我错了。

问题开始…

第一次客户用 Windows 任务管理器提供的具体内存使用数字打电话给我时,我就知道我麻烦了。Jack 是个销售人员,但通过自愿试用我的新桌面应用程序,他不知不觉地踏上了一条内存泄漏意识的速成课程。“自从今天早上重启以来,内存已经涨到五十万 K 了,”他说,“我该怎么办?”

那天是星期五下午,我周末要去外地参加婚礼。Jack 前一天已经注意到了这个问题,我建议暂时解决办法是关闭再重新打开。像所有好的 beta 测试人员一样,他很乐意接受临时解决方案,但像所有出色的 beta 测试人员一样,他不愿意接受一遍又一遍地执行临时修复。

Jack 甚至不是应用程序最重度的用户,而且我知道他机器上安装的内存都高于平均水平,所以在我能追踪和修复这个泄漏之前,不可能上线。问题每分每秒都在增长:计划的上线日期是周一,而我一直在外面奔波,所以自内存问题出现以来,我都无法审查代码。

仅凭任务管理器和 Google 与内存泄漏作斗争

我星期天晚上回到家,在搜索引擎上搜寻,试图学习 C# 内存管理的基础知识。不过,我公司的应用程序规模庞大,而我只有任务管理器来告诉我它在任何给定时间使用了多少内存。

显示发票似乎是问题的一部分;这是一个涉及大量不同元素的流程:一个选项卡页面,页面上的一个用户控件,以及该用户控件内的约一百个其他控件,包括一个从 .Net ListView 派生的复杂网格控件,该控件出现在应用程序的几乎所有屏幕上。每次显示发票时,内存使用都会跳转,但关闭选项卡并不会释放内存。我设置了一个测试流程,连续显示和关闭 100 张发票,并测量平均内存变化。哦不。每张发票损失至少 300k。

此时已经是星期天晚上大约 8 点了,不用说,我开始出汗了。我们第二天必须上线。我们已经接近了我们最初时间估计的尾声,其他项目正在堆积,客户已经开始质疑整个重新设计过程的明智性。我对 C# 的内存管理学到了很多,但无论我怎么做,似乎都无法阻止我的应用程序失控。

引入 ANTS

这时,我注意到一个 ANTS Profiler 的横幅广告,这是一个用于 .NET 的内存分析器。我下载并安装了免费试用版,在心里构思了第二天早上必须写的那封道歉邮件“请再给我几天时间”,如果找不到解决方案的话。

当我打开它时,ANTS 的工作方式很清楚。它只需要 .exe 文件的路径,然后启动系统并开启内存监控。我完成了应用程序的登录过程,然后使用 ANTS 的主要功能,在显示任何发票或其他屏幕之前,对应用程序的内存配置文件进行“快照”。

浏览第一个配置文件快照时,我被可用的信息量震惊了。我一直在试图用任务管理器中的一个内存使用数字来查明问题,而现在我拥有一个包含程序正在使用的每个活动对象的按实例列表。ANTS 允许我按命名空间(.NET 的以及我自己的)、按类、按总内存使用量、按实例计数以及我可能想知道的任何其他信息来排序项目。

有了这些信息,我暂时搁置了我的道歉邮件,打开了我的应用程序,运行了显示 100 张发票的流程,然后又拍了一张快照。回到 ANTS 中,我检查了主发票显示用户控件实例的列表。在那里,我有 100 个该控件的实例,以及 100 个选项卡实例和 100 个其他所有内容的实例,尽管屏幕上的所有选项卡都已经关闭。

显而易见的问题:对象未从数组中移除

在我的研究中,我了解到 .NET 内存管理模型使用实例的引用树来确定是否删除它。在 ANTS 中多点击几下,我发现它可以向我显示程序中每个实例的所有引用,包括到和自的引用。

使用 ANTS 在链接引用的迷宫中向前和向后导航,我很快就找到了一个静态的 ArrayList,所有显示的选项卡都被添加到了其中,但从未从中移除。

添加了几行代码将每个选项卡在关闭时从该集合中移除后,我重新运行了分析器和 100 张发票的流程,结果:选项卡、主用户控件以及几乎所有的子控件都消失了。情况甚至变得更好:每张发票后的内存增加量降至原来的五分之一,这使得内存泄漏从一个主要问题降为一个小的烦恼。第二天我们上线了,尽管出现了各种大小的问题,但没有一个是由泄漏引起的。

更微妙的问题:事件监听器和 ListView 对象

然而,那周晚些时候,Jack 的电话又来了:“内存仍在缓慢增加;怎么回事?”我不知道,但至少我现在知道去哪里找了。我使用 ANTS Profiler 看看是否能找到剩余的泄漏。我发现主发票用户控件的一个子控件,那个基于 ListView 的控件,它是界面的主要组成部分,被所谓的标准事件处理程序(如 OnClickMouseMove)保留在引用树中。我以为这些是通过 Visual Studio IDE 添加的钩子,它们应该会被自动清除。

这让我很困惑,我写信给 ANTS 系统的开发者 Red Gate Software,寻求一些额外的帮助。他们的支持人员迅速回复,并解释说,在存在大量复杂引用和事件处理程序的情况下,.NET 运行时有时会在本应被处置时保留事件处理程序。他们建议手动在导致问题的用户控件的 Dispose 方法中移除每个处理程序。

我添加了大约 20 个 minus-equals 语句来移除每个处理程序,并且为了保险起见,我在选项卡关闭过程之后添加了一个 System.GC.Collect() 语句。

重新运行 ANTS Profiler 和 100 张发票的流程,我发现内存使用量保持稳定。然后,在重新检查 ANTS Profiler 快照时,我可以看到所有与发票相关的控件都已释放,并且任务管理器中的内存使用数字从未移动过。

我重新编译并上传了新版本。现在轮到我打电话给 Jack 了。

摘要

我从这一切中学到了什么?首先,“它是托管代码,所以我们不必担心内存泄漏”的假设已经不准确了。

尽管 .NET 中的自动内存管理让 .NET 开发者的生活变得轻松多了,但在应用程序中引入内存泄漏仍然很容易。即使在托管内存中,也可能存在问题。内存管理器无法释放仍然“活动”的内存——如果它通过连接各种对象的“蜘蛛网”的直接或间接引用,它仍然被认为是活动的。此外,当涉及复杂的引用树和事件处理程序时,内存管理器并不总是取消注册这些事件处理程序,因此除非你强制释放内存,否则它将永远不会被释放。

其次,我学到了一点,仅凭任务管理器来追踪这类问题根本不可能——尤其是在我给定的时间范围内。任务管理器(和性能监视器)等工具能够告诉我我的应用程序使用了大量内存,但我需要一个专用的内存分析器,如 ANTS Profiler,才能真正向我展示构成该内存的对象以及它们为何仍然存在。

© . All rights reserved.