dotMemory 2025.3 Help

查找内存泄漏

示例应用程序

在本教程中,我们将了解如何使用 dotMemory 定位和修复应用程序中的内存泄漏。 但在继续之前,让我们先统一对内存泄漏的定义。

什么是内存泄漏?

根据最流行的定义,内存泄漏是由于内存管理不当导致的结果,当“一个对象存储在内存中但无法被运行的代码访问”时会发生。此外,“内存泄漏会随着时间累积,如果不清理,系统最终会耗尽内存。”

实际上,如果我们严格遵循上述定义,“经典”内存泄漏在 .NET 应用程序中是不可能发生的。 垃圾回收器(GC)完全控制内存释放,并移除所有无法被代码访问的对象。 此外,在应用程序关闭后,GC 会完全释放应用程序占用的内存。 然而,第 2 点(由于泄漏导致的内存耗尽)是非常真实的。 当然,这不会导致系统崩溃,但迟早应用程序会引发 OutOfMemory 异常。

为什么会发生这种情况? 问题在于,GC 只会收集 未引用的对象。 如果有一个您不知道的对象引用,GC 将不会收集该对象。 因此,修复内存泄漏的主要策略是确定随着时间累积的对象(导致泄漏)以及将前者保留在内存中的对象。

让我们尝试在示例应用程序中使用这种策略修复泄漏。

示例应用程序

再次说明,我们将在本教程中使用的应用程序是 Conway 的生命游戏。 在继续之前,请从 github下载该应用程序。 假设我们想要收回一些用于开发生命游戏的资金,并决定添加一个窗口向用户展示各种广告。 按照最差的实践,我们在每次用户启动生命游戏(点击 启动 按钮)时显示广告窗口。 当用户点击横幅时,他/她会被重定向到某个网站,广告窗口会关闭(用户也可以使用标准关闭按钮关闭窗口,尽管这并不是我们真正想要的)。 为了更换广告,广告窗口使用了一个基于 DispatcherTimer 类的计时器。 您可以在 AdWindow.cs 文件中查看 AdWindow 类的实现。

T2 Gol App

因此,功能已添加,现在是测试它的最佳时机。 让我们运行 dotMemory,确保广告窗口不会影响应用程序的内存使用(换句话说,它被正确分配和回收)。

步骤 1。 运行 dotMemory

  1. 在 Visual Studio 中打开生命游戏解决方案。

  2. 通过菜单 ReSharper | Profile | 运行启动项目内存分析... 运行 dotMemory。

    T2 Resharper Menu Upd D M

    这将打开 分析器配置 窗口。

  3. 分析器配置 窗口中,选择 从启动时收集内存分配和流量数据。 这将告诉 dotMemory 在应用程序启动后立即开始收集分析数据。 以下是您指定选项后窗口的外观:

    T2 Profiler Conf
  4. 点击 运行 以开始分析会话。 这将运行我们的应用程序并在 dotMemory 中打开主分析页面。

步骤 2。 获取快照

一旦应用程序运行,我们就可以获取内存快照。 由于我们想测试新的广告窗口及其对内存使用的影响,我们需要获取两个快照:一个是在窗口显示后立即获取(我们将使用此快照作为比较的基础),另一个是在广告窗口关闭后获取。 第二个快照是为了确保 GC 从内存中移除了我们的窗口。

  1. 通过点击应用程序中的 启动 按钮启动游戏。 广告窗口将会出现。

    T2 Gol App
  2. 在 dotMemory 中点击 获取快照 按钮。

    T2 Get Snapshot1

    这将捕获数据并将快照添加到快照区域。 获取快照不会中断分析过程,因此可以让我们获取另一个快照。

  3. 关闭应用程序中的广告窗口。

  4. 再次点击 dotMemory 中的 获取快照 按钮获取快照。

  5. 通过关闭生命游戏应用程序结束分析会话。 主页面现在包含两个快照。

    T2 Get Snapshot2

步骤 3。 比较快照

现在,我们将比较和对比收集的两个快照。 我们想看到什么? 如果一切正常,广告窗口应该出现在第一个快照中,但在第二个快照中消失。 让我们来看一下。

  1. 点击 添加到比较 将每个快照添加到比较区域。 添加快照的顺序并不重要,因为 dotMemory 总是使用较旧的快照作为比较的基础。

    T2 Snapshot Comparison Area
  2. 点击比较区域中的 比较。 这将打开 快照比较 视图。

    T2 Snapshot Comparison View

    该视图显示了在快照之间创建(新对象 列)和移除(死对象 列)的某个类的对象数量。 存活对象显示有多少对象通过了垃圾回收,换句话说,存在于两个快照中。 目前,我们关注的是 AdWindow 类。

  3. 为了更容易找到 AdWindow 类,让我们按对象所属的命名空间对所有对象进行排序。 为此,请点击表格顶部 分组依据 列表中的 命名空间

  4. 打开 GameOfLife 命名空间。

    T2 Snapshot Comparison Namespace

    那是什么? GameOfLife.AdWindow 对象位于 存活对象 列中,这意味着广告窗口仍然存在。 在我们关闭窗口后,相应的对象本应从堆中移除。 然而,有些东西阻止了它被回收。

是时候开始我们的调查,找出为什么广告窗口没有被移除了!

步骤 4。 分析快照

正如 如何开始使用 dotMemory教程中提到的,您应该将自己在 dotMemory 中的工作视为犯罪调查。 您通过分析一份庞大的嫌疑对象(对象)列表开始调查,并不断缩小范围,直到找到导致问题的那个对象。 您的推理链显示在 dotMemory 窗口左侧的所谓分析路径中。

让我们尝试这种方法实际操作一下:

  1. 打开存活的 GameOfLife.AdWindow 实例。 为此,请点击 GameOfLife.AdWindow 类旁边 存活对象 列中的数字 1

    T2 Select Snapshot

    由于该对象存在于两个快照中,dotMemory 会提示您指定在哪个快照中显示该对象。 当然,我们关注的是最后一个快照,在该快照中窗口本应被回收。

  2. 选择 在较新的快照中打开“存活对象” 并点击 确定

    T2 Adwindow Instance

    这将向我们显示实例“存在于快照 #1 和 #2 中的 AdWindow 类的实例”。 请注意,实例的可能视图列表与对象集的视图列表不同。 例如,对象实例的默认视图是 传出引用 ,它显示实例对其他对象的引用树。 然而,我们关注的不是 AdWindow 引用的对象,而是那些引用它的对象,换句话说,保留广告窗口在内存中的对象。 为了解决这个问题,我们可以切换到 关键保留路径 视图。 此视图显示了保留路径的图表。 请注意,该视图显示了 并非所有可能的路径 ,而是仅显示彼此差异最显著的路径。 这排除了大量非常相似的保留路径,简化了分析。

  3. 点击视图列表中的 关键保留路径

    T2 Instance Retention Paths

    正如您所见,广告窗口被事件处理程序 EventHandler 保留在内存中,而事件处理程序又被 DispatcherTimer 类的一个实例引用。

    T2 Tick Event

    DispatcherTimer 实例上方的文本为我们提供了另一个线索——该实例通过 Tick 事件处理程序被引用。 现在,让我们找出哪个方法将我们的实例订阅到 Tick 事件处理程序,并仔细查看代码。

  4. 点击图中的 EventHandler 实例。

    T2 Eventhandler Instance

    这将在默认的 传出引用 视图中打开 EventHandler 实例*。 现在,我们需要做的就是确定创建我们实例的方法。

  5. 要快速找到所需的方法,请切换到 创建堆栈跟踪 视图。

    T2 Instance Stack Trace

    找到了! 堆栈中实际创建计时器的最新调用是 AdWindow 构造函数。 让我们在代码中找到它。

  6. 切换到包含 GameOfLife 解决方案的 Visual Studio,并定位 AdWindow 构造函数。

    public AdWindow(Window owner) { ... _adTimer = new DispatcherTimer {Interval = TimeSpan.FromSeconds(3)}; _adTimer.Tick += ChangeAds; _adTimer.Start(); }

    正如您所见,我们的广告窗口使用 ChangeAds 方法来处理事件。 但为什么在我们关闭广告窗口后,它仍然保留在内存中? 问题在于,我们将窗口订阅到了计时器的事件,但忘记取消订阅。 因此,修复此泄漏的方法非常简单:我们需要添加一个 Unsubscribe() 方法,该方法应在关闭广告窗口时调用。 实际上,代码中已经包含了这样的方法,您需要做的只是取消注释窗口 OnClosed 事件中的 Unsubscribe(); 行。 最后,代码应如下所示:

    protected override void OnClosed(EventArgs e) { Unsubscribe(); base.OnClosed(e); } public void Unsubscribe() { _adTimer.Tick -= ChangeAds; }
  7. 现在,为了确保泄漏已修复,让我们构建解决方案并再次运行分析。 为此,您可以重复以下步骤 步骤 2。 获取快照步骤 3。 比较快照

    T2 Snapshot Comparison Fixed

    就是这样! AdWindow 实例现在位于 死对象 列中,这意味着在获取第二个快照时它已成功被回收。 泄漏已修复!

说实话,这种类型的泄漏确实经常发生。 事实上,频繁到 dotMemory 自动检查您的应用程序是否存在此类泄漏。

因此,如果您打开包含泄漏的第二个快照并查看 检查 视图,您会注意到 事件处理程序泄漏 检查中已经包含 AdWindow 对象。

T2 Inspections

步骤 5。 检查其他泄漏

我们已经修复了事件处理程序泄漏,现在广告窗口已成功被垃圾回收器回收。 但导致泄漏的计时器呢? 如果一切正常,计时器也应该被回收,并且在第二个快照中不存在。 让我们来看一下。

  1. 在 dotMemory 中打开第二个快照。 为此,请点击分析路径中的 分析 GameOfLife.exe 步骤(调查的起点),然后点击第二个快照的 快照 #2 链接。

    T2 2nd Leak Session
  2. 通过点击 类型 打开快照的 类型 视图。

  3. 在打开的 类型 视图中,在过滤字段中输入 dispatchertimer。 这将缩小列表范围,仅保留类名中包含此模式的对象。 正如您所见,堆中有 7 个 System.Windows.Threading.DispatcherTimer 对象。

    T2 2nd Leak Type List
  4. 通过双击打开此对象集。

    T2 2nd Leak Timer Obj Set

    这将在 类型 视图中打开该集合。 现在,我们需要确保此集合不包含由广告窗口创建的计时器。 由于计时器是在 AdWindow 构造函数中创建的,最简单的方法是使用 反向跟踪 视图查看集合。

  5. 点击视图列表中的 反向跟踪。 该视图将显示从直接创建对象的调用开始,并向下到堆栈中的第一个调用。

    T2 2nd Leak Back Traces

    不幸的是, AdWindow.ctor(Window owner) 调用仍然存在,这意味着由此调用创建的计时器未被回收。 它存在于快照中,尽管广告窗口已关闭并从内存中移除。 这看起来像是另一个需要我们分析的内存泄漏。

  6. 双击 AdWindow.ctor(Window owner) 调用。 dotMemory 将向我们显示由此调用创建的 DispatcherTimer 类的实例。 默认情况下,将使用 传出引用 视图。 而我们想要找出该实例是如何保留在内存中的。 因此,让我们使用 关键保留路径 视图。

  7. 点击 关键保留路径。 正如您所见,有两条主要的保留路径。

    T2 2nd Leak Key Paths

    计时器的第一条保留路径将我们引向 DispatcherTimer 列表,该列表是全局的,存储了应用程序中的所有计时器。 第二条路径显示计时器也被 DispatcherOperationCallback 对象保留。 此对象是您运行计时器时创建的委托。 这意味着计时器仍在运行。 DispatcherTimer 类的一个特殊之处在于,只有在计时器停止后,实例才会从全局计时器列表中移除。 因此,为了修复泄漏,我们必须在广告窗口关闭之前停止计时器。 让我们在代码中完成此操作!

  8. 打开包含 AdWindow 类实现的 AdWindow.cs 文件。 实际上,修复将非常简单。 我们需要做的只是将 adTimer.Stop(); 行添加到 Unsubscribe() 方法中。 修复后,该方法应如下所示:

    public void Unsubscribe() { _adTimer.Tick -= ChangeAds; _adTimer.Stop(); }
  9. 重新构建解决方案。

  10. 重复 步骤 2。 获取快照

  11. 类型 视图中打开第二个快照,并找到所有 System.Windows.Threading.DispatcherTimer 类型的对象。

    T2 2nd Leak Fixed

    正如您所见,现在只有 6 个 DispatcherTimer 对象,而不是 7 个。 为了确保垃圾回收器回收了广告窗口使用的计时器,让我们使用 反向跟踪 视图查看这些计时器。

  12. 双击 DispatcherTimer 对象,然后在视图列表中点击 反向跟踪

    T2 2nd Leak Fixed Back Traces

    太好了! 列表中没有 AdWindow 构造函数,这意味着泄漏已成功修复。

当然,这种类型的泄漏看起来并不严重,尤其是对于我们的应用程序。 如果我们没有使用 dotMemory,可能永远都不会注意到这个问题。 然而,在其他应用程序中(例如,24/7 运行的服务器端应用程序),这种泄漏可能会在一段时间后通过引发 OutOfMemory 异常而显现出来。

最后修改日期: 2025年 12月 8日