录制堆快照

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

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

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

拍摄快照

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

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

选定的分析类型和 JavaScript 虚拟机实例。

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

可到达对象的总大小。

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

分散 Item 对象的堆快照。

清除快照

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

清除所有配置文件。

查看快照

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

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

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

摘要视图

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

具有展开构造函数的 Summary 视图。

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

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

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

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

构造函数过滤器

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

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

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

摘要中的特殊条目

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

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

构造函数条目。

(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 属性保留。

忽略保留器

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

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

如需隐藏某个保留器,请右键点击该保留器,然后选择忽略此保留器。忽略的保留器在 Distance 列中标记为 ignored。如需停止忽略所有保留器,请点击顶部操作栏中的 恢复忽略的保留器

查找特定对象

如需在收集的堆中查找某个对象,您可以使用 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 子树