Missed the action at the 2018 Chrome Dev Summit? Catch up with our playlist on the Google Chrome Developers channel on YouTube. Watch now.

内存术语

本部分将介绍内存分析中的常用术语,适用于不同语言的各种内存分析工具。

此处介绍的术语和概念适用于 Chrome DevTools 堆分析器。如果您之前使用过 Java、.NET 或其他内存分析器,那么本部分内容对您来说将是全新的。

对象大小

将内存视为具有原语类型(如数字和字符串)和对象(关联数组)的图表。形象一点,可以将内存表示为一个由多个互连的点组成的图表,如下所示:

内存的直观表示

对象可通过以下两种方式占用内存:

  • 直接通过对象自身占用。

  • 通过保持对其他对象的引用隐式占用,这种方式可以阻止这些对象被垃圾回收器(简称 GC)自动处置。

使用 DevTools 中的堆分析器(一种用于检查在“Profiles”下发现的内存问题的工具)时,您会看到多个信息列。Shallow SizeRetained Size 这两个列比较引人注目,但它们表示什么呢?

浅层大小和保留大小

浅层大小

这是对象自身占用内存的大小。

典型的 JavaScript 对象会将一些内存用于自身的说明和保存中间值。通常,只有数组和字符串会有明显的浅层大小。不过,字符串和外部数组的主存储一般位于渲染器内存中,仅将一个小包装器对象置于 JavaScript 堆上。

渲染器内存是渲染检查页面的进程的内存总和:原生内存 + 页面的 JS 堆内存 + 页面启动的所有专用工作线程的 JS 堆内存。尽管如此,即使一个小对象也可能通过阻止其他对象被自动垃圾回收进程处理的方式间接地占用大量内存。

保留大小

这是将对象本身连同其无法从 GC 根到达的相关对象一起删除后释放的内存大小。

GC 根句柄组成,这些句柄在从原生代码引用 V8 外部的 JavaScript 对象时创建(本地或全局)。所有此类句柄都可以在 GC 根 > 句柄作用域GC 根 > 全局句柄下的堆快照内找到。本文档对句柄的介绍没有深入到浏览器实现的细节,可能让您感到困惑。您不必担心 GC 根和句柄。

存在很多内部 GC 根,其中的大部分都不需要用户关注。从应用角度来看,存在以下种类的根:

  • Window 全局对象(位于每个 iframe 中)。堆快照中有一个距离字段,表示从 window 出发的最短保留路径上的属性引用数量。

  • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成。并不是所有的节点都有 JS 包装器,不过,如果有包装器,并且文档处于活动状态,包装器也将处于活动状态。

  • 有时,对象可能会被调试程序上下文和 DevTools 控制台保留(例如,在控制台评估后)。在调试程序中清除控制台并移除活动断点,创建堆快照。

内存图从根开始,根可以是浏览器的 window 对象或 Node.js 模块的 Global 对象。您无法控制此根对象的垃圾回收方式。

无法控制的对象

任何无法从根到达的对象都会被 GC 回收。

对象保留树

堆是一个由互连的对象组成的网络。在数学领域,此结构被称为“图表”或内存图。图表由通过边缘连接的节点组成,两者都是给定标签。

  • 节点或对象)使用构造函数(用于构建节点)的名称进行标记。
  • 边缘使用属性的名称进行标记。

了解如何使用堆分析器记录分析。我们可以从下面的堆分析器记录中看到一些引人注目的参数,例如距离:距离是指与 GC 根之间的距离。如果相同类型的几乎所有对象的距离都相同,只有少数对象的距离偏大,则有必要进行调查。

距根的距离

支配项

支配对象由一个树结构组成,因为每个对象都有且仅有一个支配项。对象的支配项可能缺少对其所支配对象的直接应用;也就是说,支配项的树不是图表的生成树。

在下面的图表中:

  • 节点 1 支配节点 2
  • 节点 2 支配节点 3 、4 和 6
  • 节点 3 支配节点 5
  • 节点 5 支配节点 8
  • 节点 6 支配节点 7

支配项树结构

在下面的示例中,节点 #3#10 的支配项,但 #7 也存在于从 GC 到 #10 的每一个简单路径中。因此,如果对象 B 存在于从根到对象 A 的每一个简单路径中,那么对象 B 就是对象 A 的支配项。

支配项动画图示

V8 详细信息

分析内存时,了解堆快照的显示方式非常有用。本部分将介绍一些特定于 V8 JavaScript 虚拟机(V8 VM 或 VM)的内存相关主题。

JavaScript 对象表示

存在三种原语类型:

  • 数字(例如 3.14159..)
  • 布尔值(true 或 false)
  • 字符串(例如“Werner Heisenberg”)

它们无法引用其他值,并且始终是叶或终止节点。

数字可以存储为:

  • 中间 31 位整型值(称为小整型 (SMI)),或
  • 堆对象,作为堆数字引用。堆数字用于存储不适合 SMI 格式的值(例如双精度),或者在需要将值“包装”起来时使用(例如在值上设置属性)。

字符串可以存储在以下位置:

  • VM 堆中,或
  • 渲染器内存中(外部)。将创建一个包装器对象并用于访问外部存储空间,例如,外部存储空间是存储脚本源和从网页接收(而不是复制到 VM 堆上)的其他内容的位置。

新 JavaScript 对象的内存分配自专用的 JavaScript 堆(或 VM 堆)。这些对象由 V8 的垃圾回收器管理,因此,只要存在一个对它们的强引用,它们就会一直保持活动状态。

原生对象是 JavaScript 堆之外的任何对象。与堆对象相反,原生对象在其生命周期内不由 V8 垃圾回收器管理,并且只能使用其 JavaScript 包装器对象从 JavaScript 访问。

Cons 字符串是一种由存储并联接的成对字符串组成的对象,是串联的结果。cons 字符串内容仅根据需要进行联接。一个示例便是需要构造已联接字符串的子字符串。

例如,如果您将 ab 串联,您将获得一个字符串 (a, b),它表示串联结果。如果您稍后将 d 与该结果串联,您将得到另一个 cons 字符串 ((a, b), d)。

数组 - 数组是一个具有数字键的对象。它们在 V8 VM 中广泛使用,用于存储大量数据。用作字典的成套键值对采用数组形式。

典型的 JavaScript 对象可以是两个数组类型之一,用于存储:

  • 命名属性,以及
  • 数字元素

数字元素如果属性数量非常少,可以将其存储在 JavaScript 对象自身内部。

映射 - 一种用于说明对象种类及其布局的对象。例如,可以使用映射说明用于快速属性访问的隐式对象层次结构。

对象组

每个原生对象组都由保持对彼此的相互引用的对象组成。例如,在 DOM 子树中,每个节点都有一个指向其父级的链接,并链接到下一个子级和下一个同级,形成一个互连图。请注意,原生对象不会在 JavaScript 堆中表示 - 这正是它们的大小为什么为零的原因。相反,创建包装器对象。

每个包装器对象都会保持对相应原生对象的引用,用于将命令重定向到自身。这样,对象组会保持包装器对象。不过,这不会形成一个无法回收的循环,因为 GC 非常智能,可以释放包装器对象不再被引用的对象组。但是,忘记释放单个包装器将保持整个组和关联的包装器。