录制堆快照

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

了解如何依次选择内存 > 性能分析 > 堆快照来记录堆快照以及如何查找内存泄漏。

堆分析器会按网页的 JavaScript 对象和相关 DOM 节点显示内存分配情况。使用分析器可以拍摄 JS 堆快照、分析内存图、比较快照以及查找内存泄漏。如需了解详情,请参阅对象保留树

拍摄快照

如需拍摄堆快照,请执行以下操作:

  1. 在要分析性能的网页上,打开 DevTools 并前往内存面板。
  2. 选择 堆快照性能分析类型,然后选择一个 JavaScript 虚拟机实例,并点击拍摄快照

所选的性能分析类型和 JavaScript 虚拟机实例。

内存面板加载并解析快照后,会在堆快照部分的快照标题下方显示可到达的 JavaScript 对象的总大小。

可到达对象的总大小。

快照仅显示可从全局对象访问的内存图中的对象。拍摄快照始终从垃圾回收开始。

分散 Item 对象的堆快照。

清除快照

如要移除所有快照,请依次点击 清除所有配置文件

清除所有配置文件。

查看快照

如需出于不同目的从不同角度检查快照,请从顶部的下拉菜单中选择一个视图:

查看 内容 用途
摘要 按构造函数名称分组的对象。 使用此视图可以根据类型深入了解对象及其内存使用。有助于跟踪 DOM 泄漏
比较 两个快照之间的差异。 使用此视图可以比较两个(或多个)快照在某个操作前后的差异。通过检查已释放内存的变化和参考计数,确认是否存在内存泄漏及其原因。
容器化 堆内容 此视图提供了一种更好的对象结构视图,有助于分析全局命名空间 (window) 中引用的对象以找出是什么让它们始终如影随形。使用此视图可以分析闭包以及在较低级别深入了解您的对象。
统计信息 内存分配饼图 查看分配给代码、字符串、JS 数组、类型化数组和系统对象的内存部分的相对大小。

从顶部的下拉菜单中选择的“摘要”视图。

摘要视图

堆快照最初会在 Summary 视图中打开,并在一个列中列出构造函数。您可以展开构造函数以查看它们实例化的对象。

展开的构造函数的“摘要”视图。

如需滤除不相关的构造函数,请在摘要视图顶部的类过滤器中输入要检查的名称。

构造函数名称旁边的数字表示使用该构造函数创建的对象总数。摘要视图还会显示以下列:

  • 距离:显示使用节点最短简单路径时距根节点的距离。
  • 浅层大小显示通过特定构造函数创建的所有对象浅层大小的总和。浅层大小是指对象自身占用的内存大小。一般来说,数组和字符串的浅层大小比较大。另请参阅对象大小
  • 保留大小显示同一组对象中最大的保留大小。保留大小是指通过删除某个对象并使其依赖项不再可到达而可以释放的内存大小。另请参阅对象大小

展开构造函数后,摘要视图会显示其所有实例。每个实例的浅层大小和保留大小在相应的列中会显示明细。@ 字符后面的数字是对象的唯一 ID。您可以使用此 ID 以对象为基础比较堆快照。

构造函数过滤器

借助摘要视图,您可以根据内存用量低效的常见情况过滤构造函数。

如需使用这些过滤条件,请从操作栏中最右侧的下拉菜单中选择以下任一选项:

  • 所有对象:当前快照捕获的所有对象。默认设置。
  • 在快照 1 之前分配的对象:在拍摄第一个快照之前创建并保留在内存中的对象。
  • 在快照 1 和快照 2 之间分配的对象:查看最新快照与上一个快照之间的对象差异。每次创建新的快照都会向下拉列表中添加此过滤条件的增量。
  • 重复字符串:在内存中多次存储的字符串值。
  • 由已分离的节点保留的对象:由于已分离的 DOM 节点引用了这些对象,因此这些对象保持活跃状态。
  • 由开发者工具控制台保留的对象:由于通过开发者工具控制台评估或与之互动,而保留在内存中的对象。

“摘要”中的特殊条目

除了按构造函数分组之外,摘要视图还会按以下条件对对象进行分组:

  • 内置函数,例如 ArrayObject
  • 按标记(例如 <div><a><img> 等)分组的 HTML 元素。
  • 您在代码中定义的函数。
  • 不基于构造函数的特殊类别。

构造函数条目。

(array)

此类别包括各种内部数组类对象,这些对象与 JavaScript 中可见的对象不直接对应。

例如,JavaScript Array 对象的内容存储在名为 (object elements)[] 的次级内部对象中,以便更轻松地调整大小。同样,JavaScript 对象中的命名属性通常存储在名为 (object properties)[] 的次级内部对象中,这些对象也列在 (array) 类别中。

(compiled code)

此类别包含 V8 运行由 JavaScript 或 WebAssembly 定义的函数所需的内部数据。每个函数都可以以多种方式表示,从小而慢到大而快。

V8 会自动管理此类别的内存用量。如果某个函数运行多次,V8 会为该函数使用更多内存,以便其运行更快。如果某个函数在一段时间内未运行,V8 可能会清除该函数的内部数据。

(concatenated string)

V8 串联两个字符串(例如使用 JavaScript + 运算符)时,它可能会选择在内部将结果表示为“串联字符串”,也称为 Rope 数据结构。

V8 会分配一个小对象,其中包含名为 firstsecond 的内部字段,这些字段指向两个源字符串,而不是将两个源字符串的所有字符复制到新字符串中。这样,V8 就可以节省时间和内存。从 JavaScript 代码的角度来看,这些只是普通字符串,其行为与任何其他字符串一样。

InternalNode

此类别表示在 V8 之外分配的对象,例如 Blink 定义的 C++ 对象。

如需查看 C++ 类名称,请使用 Chrome 测试版,然后执行以下操作:

  1. 打开 DevTools,然后依次开启 Settings > Experiments > Show option to expose internals in heap snapshots
  2. 打开内存面板,选择 堆快照,然后开启 公开内部设置(包括因实现而异的更多详情)
  3. 重现导致 InternalNode 保留大量内存的问题。
  4. 拍摄堆快照。在此快照中,对象具有 C++ 类名称,而不是 InternalNode
(object shape)

V8 中的快速属性中所述,V8 会跟踪隐藏类(或形状),以便高效地表示具有相同属性且顺序相同的多个对象。此类别包含名为 system / Map(与 JavaScript Map 无关)的隐藏类和相关数据。

(sliced string)

V8 需要取子字符串时(例如,JavaScript 代码调用 String.prototype.substring() 时),V8 可能会选择分配切片字符串对象,而不是从原始字符串复制所有相关字符。这个新对象包含指向原始字符串的指针,并描述了要使用原始字符串中的哪个字符范围。

从 JavaScript 代码的角度来看,这些只是普通字符串,其行为与任何其他字符串一样。如果切片字符串保留了大量内存,则程序可能已触发 2869 问题,因此不妨采取有针对性的措施来“扁平化”切片字符串。

system / Context

类型为 system / Context 的内部对象包含来自闭包(嵌套函数可以访问的 JavaScript 作用域)的局部变量。

每个函数实例都包含指向其执行所在 Context 的内部指针,以便它可以访问这些变量。虽然 Context 对象无法直接从 JavaScript 中看到,但您可以直接控制它们。

(system)

此类别包含各种尚未(或还未)以更有意义的方式进行分类的内部对象。

比较视图

通过 Comparison 视图,您可以通过相互比较多个快照来查找泄漏的对象。例如,执行某个操作并将其还原(例如打开文档并将其关闭)不应留下额外的对象。

如需验证某项操作是否会造成内存泄露,请执行以下操作:

  1. 在执行操作之前,请先获取堆快照。
  2. 执行操作。也就是说,以您认为可能会导致泄露的方式与网页互动。
  3. 执行相反的操作。也就是说,执行相反的互动,并重复几次。
  4. 再获取一个堆快照,并将其视图更改为 Comparison,将其与 Snapshot 1 进行比较。

Comparison 视图会显示两个快照之间的差异。展开概览条目时,将显示已添加和删除的对象实例:

与快照 1 相比。

Containment 视图

Containment 视图是您应用的对象结构的“俯瞰视图”。利用此视图,您可以深入了解函数闭包、观察共同组成您的 JavaScript 对象的 VM 内部对象,以及从一个非常低的级别了解您的应用使用的内存量。

此视图提供了多个入口点:

  • DOMWindow 对象。JavaScript 代码的全局对象。
  • GC 根。虚拟机垃圾回收器使用的 GC 根。GC 根可以由内置对象映射、符号表、虚拟机线程堆栈、编译缓存、句柄作用域和全局句柄组成。
  • 原生对象。“推送”至 JavaScript 虚拟机内以允许自动化的浏览器对象,例如 DOM 节点和 CSS 规则。

Containment 视图。

“保留器”部分

Memory 面板底部的 Retainers 部分会显示指向视图中所选对象的对象。当您在统计视图以外的任何视图中选择其他对象时,内存面板会更新保留器部分。

“预授权”部分。

在此示例中,所选字符串由 Item 实例的 x 属性保留。

忽略保留器

您可以隐藏保留器,以查看是否有任何其他对象保留了所选对象。使用此选项时,您无需先从代码中移除此保留器,然后重新拍摄堆快照。

下拉菜单中的“忽略此预授权”选项。

如需隐藏某个保留器,请右键点击该保留器,然后选择忽略此保留器。被忽略的预订会在“距离”列中标记为 ignored。如需停止忽略所有保留对象,请点击顶部操作栏中的 Restore ignored retainers(恢复忽略的保留对象)。

查找特定对象

如需在收集的堆中查找某个对象,您可以使用 Ctrl + F 进行搜索,然后输入对象 ID。

为函数命名以区分闭包

为函数命名非常有用,因为这样您就可以在快照中区分不同的闭包。

例如,以下代码未使用已命名的函数:

function createLargeClosure() {
  var largeStr = new Array(1000000).join('x');

  var lC = function() { // this is NOT a named function
    return largeStr;
  };

  return lC;
}

而下面的示例则使用了已命名的函数:

function createLargeClosure() {
  var largeStr = new Array(1000000).join('x');

  var lC = function lC() { // this IS a named function
    return largeStr;
  };

  return lC;
}

闭包中的命名函数。

发现 DOM 泄漏

堆分析器可以反映浏览器原生对象(DOM 节点和 CSS 规则)与 JavaScript 对象之间的双向依赖关系。这样有助于发现由被遗忘的已分离 DOM 子树引起的不可见泄漏。

DOM 泄漏可能比您想象的要大。请参考以下示例。何时回收 #tree 垃圾?

  var select = document.querySelector;
  var treeRef = select("#tree");
  var leafRef = select("#leaf");
  var body = select("body");

  body.removeChild(treeRef);

  //#tree can't be GC yet due to treeRef
  treeRef = null;

  //#tree can't be GC yet due to indirect
  //reference from leafRef

  leafRef = null;
  //#NOW #tree can be garbage collected

#leaf 可以维持对其父级 (parentNode) 的引用,并以递归方式返回 #tree,因此,只有 leafRef 被作废后,#tree 下的整个树才会成为 GC 的候选。

DOM 子树