开始使用 dotMemory
示例应用程序 | |
在本教程中,我们将学习如何运行 dotMemory 并获取内存快照。 此外,我们还将简要了解 dotMemory 的用户界面和基本分析概念。 将本教程视为您使用 dotMemory 的起点。
基本术语
您可能会问:“什么是内存快照,为什么需要获取它们?”这是一个很好的时机来了解一些您在使用 dotMemory 时会遇到的内存分析术语。
从内存的角度来看,您的应用程序的工作包括为新对象连续分配内存,以及释放不再被应用程序使用的对象所占用的内存。 对象一个接一个地分配在所谓的托管堆中。 基于此,内存分析器必须能够执行两个基本操作:
获取一个 内存快照。 快照是托管堆的瞬时图像。 每个快照都包含您的应用程序在您点击 获取快照 按钮时分配到内存中的所有对象的信息。
收集 内存流量信息。 内存流量显示了在两个内存快照之间分配和释放了多少内存。 这些信息也非常有价值,因为它可以帮助您了解应用程序的性能动态。
收集流量和获取快照的时间间隔(或者换句话说,分析您的应用程序)称为 分析会话。
当然,在学习本教程的过程中,您还会接触到其他一些术语。 但目前这些已经足够让您理解接下来的几个步骤中发生的事情。 让我们开始吧!
示例应用程序
首先,我们需要一个用于分析的应用程序。 在整个 dotMemory 教程系列中,我们将使用相同的 C# 应用程序。 它模拟了经典的康威生命游戏,大多数人可能都知道。 如果不了解,请查看 维基百科。 这不会花费您太多时间,但会让您更容易理解教程内容。 因此,在开始之前,请从 github下载该应用程序。

步骤 1。 运行 dotMemory
通过 Windows 开始菜单运行 dotMemory。

这将打开 dotMemory 的主窗口。

现在让我们开始一个分析会话(dotMemory 将收集内存使用数据的时间段)。
在左侧面板中选择 本地 ,并在 分析应用程序 中选择 独立应用程序。
现在我们需要配置分析会话选项。 在右侧面板中:
在 应用程序 中,指定我们生命游戏可执行文件的路径。 建议您分析应用程序的 Release 构建*。
选择 从启动时收集内存分配和流量数据 选项。 这将告诉 dotMemory 在应用程序启动后立即开始收集分配调用堆栈数据。
以下是您指定所有选项后窗口的外观:

点击 运行 以开始分析会话。 这将运行我们的应用程序并在 dotMemory 中打开一个新的 分析 选项卡。
步骤 2。 获取快照
一旦应用程序运行,我们就可以获取内存快照。 此操作中最重要的是选择正确的时机。 如您所记得,快照是应用程序托管堆的瞬时图像。 因此,在获取快照之前,您首先需要将应用程序置于您感兴趣的状态。 例如,如果我们想查看生命游戏启动后立即创建的对象,我们必须在应用程序中执行任何操作之前获取快照。 相反,如果我们需要知道动态创建的对象,我们必须在应用程序中点击 启动 后获取快照。
假设我们需要获取有关生命游戏运行时分配的对象的信息。 因此,在应用程序中点击 启动 按钮,让游戏运行一段时间。
在 dotMemory 中点击 获取快照 按钮。

这将捕获数据并将快照添加到快照区域。 获取快照不会中断分析过程,因此可以让我们获取另一个快照(目前不需要)。
通过关闭生命游戏窗口结束分析会话。
查看 dotMemory。 主页面现在包含一个已获取的快照及其基本信息。

总计 114.47 MB 表示应用程序总共消耗了 114.47 MB 的内存。 此大小等于 Windows 任务管理器的 提交大小 :进程请求的内存量。 总值包括:
非托管内存:在托管堆之外分配且不由垃圾回收器管理的内存。 通常,这是 .NET CLR、动态库、图形缓冲区(尤其是大量使用图形的 WPF 应用程序)等所需的内存。 这部分内存无法在分析器中进行分析。
.NET,总计 :托管堆中包括空闲内存在内的内存总量(应用程序请求但未使用的内存)。
.NET,已用 :托管堆中应用程序使用的内存量。 这是 .NET 允许您操作的唯一内存部分。 因此,这也是您能够在分析器中分析的唯一部分。
让我们更详细地查看快照。 为此,请点击 快照 #1 链接。
步骤 3。 了解快照概览
打开快照后,您首先看到的是 检查 视图。 此页面显示了主要的快照热点。

您在这里看到的是:
最大大小 :该图表显示了消耗大部分内存的对象类型。
最大的保留大小 :该图表显示了关键对象,即在内存中保留应用程序中所有其他对象的对象(有关它们的更多信息,请参阅本教程后续部分)。
字符串重复、 稀疏数组、 事件处理程序泄漏 等:为了方便您的操作,dotMemory 会自动检查快照中最常见的内存问题类型。 如果您不知道从哪里开始,这些自动检查的结果是一个很好的起点。
堆碎片 :该图表显示了托管堆段的碎片情况: 第 1 代、 2 和 大对象堆。
让我们继续检查快照并查看其中包含的对象:
点击 类型 按钮。 这将按类型对快照中的所有对象进行分组,并向您显示 类型 视图。

现在是熟悉 dotMemory 用户界面和整个内存分析“内容”的最佳时机。
步骤 4。 内存分析入门
在进一步操作之前,让我们稍作停顿,讨论对象在内存中的存储方式。 这有助于更好地理解 dotMemory 实际向您展示的内容。
内存中的对象
您的应用程序消耗的大部分内存是为应用程序的对象分配的。 对象存储数据并引用其他对象。 一个对象及其引用构成了一个 对象图。 例如, Photo 类的一个对象将自行存储 id 字段(long 值类型),并引用其他字段(引用类型的对象)。

应用程序根
当您的应用程序需要内存时,.NET 的垃圾回收器(GC)会确定并移除不再需要的对象。 为此,GC 从每个对象的图开始遍历,起点是 根 *,即静态字段、本地变量和外部句柄。 如果某个对象无法从任何根访问,则认为它不再需要,并从内存中移除。 在下面的示例中,对象 D 和 F 将从内存中移除,因为它们无法从应用程序的根访问。

保留
这里我们引入了一个关键概念: 保留。
从根到某个对象的路径可能会经过许多其他对象。 如果到对象 B 的所有路径都经过对象 A,那么 A 是 B 的 支配者。 换句话说,B 仅由 A 保留在内存中。 如果 A 被垃圾回收,B 也会被垃圾回收。 这就是为什么每个对象最重要的参数是它保留的对象的大小。 在 dotMemory 中,此参数称为 保留的字节数。 例如,下面示例中的对象 C 保留了 632 字节。 对象 B 并非完全由 C 保留,因此未包含在计算中。

让我们回到 dotMemory 并查看已打开的 类型 视图。 此视图当前显示堆中的所有对象,并按它们 独占保留的内存量排序。 如您所见,大部分内存由 System.Windows.Shapes.Ellipse 类保留(显然,这些是我们用来可视化生命游戏单元的椭圆形状)。 该类型的对象保留了 11,864,580 字节的内存,而其自身消耗了 3,862,600 字节。

一旦您熟悉了主要的分析术语,让我们看看如何使用 dotMemory。
步骤 5。 了解用户界面
我们希望您将使用 dotMemory 的工作视为某种犯罪调查(在 dotMemory 中称为 内存分析)。 这里的主要思路是收集数据(一个或多个内存快照)并选择一些嫌疑对象(可能导致问题的 分析对象)。 因此,您从一些嫌疑对象列表开始,并逐步缩小这个列表。 一个嫌疑对象可能会引导您找到另一个,依此类推,直到您确定问题的根源。
查看 dotMemory 窗口的左侧部分。 这里是 分析路径 ,显示了您所有的调查步骤。

分析路径 中的每一项都是您分析的对象。 如您所见,您从 分析 GameOfLife.exe (步骤 #1)开始,然后打开了 快照 #1 (步骤 #2),并要求 dotMemory 显示堆中的所有对象(对象集 所有对象)。 即使是一个小型应用程序也会创建大量对象,尝试单独分析每个对象并不是很有效。 这就是为什么在 dotMemory 中,分析的主要对象是所谓的 对象集。

对象集是根据特定条件选择的一组对象。 为了便于理解,可以将对象集视为某种查询的结果*(非常类似于 SQL 查询)。 例如,您可以对 dotMemory 说类似“ 选择由 SomeCall() 创建并提升到 Gen 2 的所有对象 ”或“ 选择由实例 A 保留在内存中的所有对象 ”等内容。
每个对象集都可以从不同的视角进行检查,这些视角称为 视图。 请看屏幕。 您看到的视图称为 类型 ,它显示了按类型分组的对象集中的对象的简单列表。 其他视图可以揭示有关所选对象集的其他信息。 例如, 支配者 视图将显示谁在内存中保留了所选对象; 调用树 将显示哪些调用创建了这些对象;等等。 您可以通过屏幕顶部的按钮轻松更改视图:

如上所述,您分析的每个对象可能会引导您找到另一个对象。 例如,我们看到
System.Windows.Shapes.Ellipse类保留了大部分内存,我们想知道是什么方法创建了所有这些椭圆形。 让我们找出答案。双击 System.Windows.Shapes.Ellipse ,或右键点击这些对象以打开上下文菜单,然后选择 打开此对象集。
选择 反向跟踪 视图。

该视图显示了这些椭圆形来源于
Grid.InitCellsVisuals()方法。 请注意,分析路径现在包含了一个新步骤: 按类型分组 System.Windows.Shapes.Ellipse。尝试使用 dotMemory 进行一些实验。 例如,确定 System.Windows.Shapes.Ellipse 类的对象保留了什么。