录制堆快照

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

了解如何依次选择内存 > 配置文件 > 堆快照来记录堆快照,以及如何查找内存泄漏。

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

拍摄快照

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

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

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

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

可访问对象的总大小。

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

分散 Item 对象的堆快照。

清除快照

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

清除所有配置文件。

查看快照

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

查看 内容 用途
摘要 按构造函数名称和来源分组的对象。 您可以使用它根据类型查找对象及其内存用量。有助于跟踪 DOM 泄露
比较 两个快照之间的差异。 您可以使用它来比较操作前后的两个(或更多)快照。通过检查释放的内存和引用计数的增量,确认是否存在内存泄漏以及内存泄漏的原因。
容器化 堆内容 可更好地查看对象结构,并帮助分析全局命名空间 (window) 中引用的对象,以了解这些对象的保留原因。您可以使用它来分析闭包,并深入了解低级对象。
统计信息 内存分配饼图 查看分配给代码、字符串、JS 数组、类型化数组和系统对象的内存部分的相对大小。

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

摘要视图

最初,摘要视图中会打开一个堆快照,其中列出了一个构造函数列。构造函数的命名方式与创建对象的 JavaScript 函数相同,普通对象的名称基于它们包含的属性,有些名称是特殊条目。所有对象首先按名称分组,然后按其来源源文件中的行(例如 source-file.js:line-number)分组。

您可以展开分组的构造函数,查看它们实例化的对象。

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

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

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

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

展开构造函数后,摘要视图会显示其所有实例。每个实例都会在相应列中显示其浅层大小和保留大小的细分信息。@ 字符后面的数字是对象的唯一 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. 打开开发者工具,然后依次开启 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)

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

比较视图

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

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

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

比较视图会显示两个快照之间的差异。展开总计条目后,系统会显示添加和删除的对象实例:

与快照 1 相比。

容器视图

Containment 视图是应用对象结构的“鸟瞰图”。借助它,您可以深入了解函数闭包,观察共同构成 JavaScript 对象的虚拟机内部对象,以及了解应用在非常低级别的内存用量。

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

  • 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 被置为 null 时,#tree 下的整个树才会成为 GC 的候选对象。

DOM 子树