内存术语

Meggin Kearney
Meggin Kearney

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

此处所述的术语和概念适用于 Chrome 开发者工具堆分析器。如果您用过 Java、.NET 或其他内存分析器,那么本课程会对您有所帮助。

对象大小

可以将内存视为包含基元类型(如数字和字符串)和对象(关联数组)的图表。它可直观地表示为一个图表,其中包含多个互连的点,如下所示:

内存的直观表示

对象可以通过两种方式保留内存:

  • 直接通过对象本身。
  • 通过保留对其他对象的引用来隐式处理这些对象,从而阻止这些对象被垃圾回收器(简称 GC)自动处置。

使用开发者工具中的堆分析器(一种用于调查在“Profiles”下发现的内存问题的工具)时,您可能会发现自己查看的是几个不同的信息列。Shallow SizeRetained Size 这两个标签比较引人注目,但它们表示什么呢?

浅层大小和保留大小

浅层大小

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

典型的 JavaScript 对象会预留一些内存用于自身的说明和存储立即值。通常情况下,只有数组和字符串才能有明显的浅层大小。不过,字符串和外部数组的主存储空间通常位于渲染器内存中,仅在 JavaScript 堆上公开一个小型封装容器对象。

渲染程序内存是渲染被检查页面的进程的内存:原生内存 + 页面的 JS 堆内存 + 页面启动的所有专用工作器的 JS 堆内存。不过,即使是小对象也可以阻止其他对象被自动垃圾回收进程处置,从而间接占用大量内存。

保留的大小

这是对象本身及其无法从 GC 根访问的从属对象被删除后释放的内存大小。

GC 根句柄组成,这些句柄在从原生代码引用 V8 之外的 JavaScript 对象时创建(本地或全局)。所有此类句柄都可以在堆快照中找到,具体路径如下:GC 根 > 处理范围GC 根 > 全局句柄。在本文档中描述句柄,但没有深入介绍浏览器实现细节,可能会让人感到困惑。您无需担心 GC 根和句柄。

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

  • Window 全局对象(在每个 iframe 中)。堆快照中有一个距离字段,它是距离窗口的最短保留路径的属性引用数量。
  • 文档 DOM 树,由可通过遍历文档到达的所有原生 DOM 节点组成。并非所有文档都有 JS 封装容器,但如果它们有封装容器,那么当文档处于活动状态时,封装容器将会保持活跃状态。
  • 有时,对象可能会由调试程序上下文和开发者工具控制台保留(例如,在控制台评估后)。在调试程序中清除控制台并移除活动断点,创建堆快照。

内存图从根开始,根可以是浏览器的 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 虚拟机或虚拟机)的内存相关主题。

JavaScript 对象表示法

有三种原语类型:

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

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

数字可以存储为:

  • 31 位立即数整数值(称为小整数 [SMI]),或
  • 堆对象(称为堆数字)。堆编号用于存储不适合 SMI 格式的值(例如双精度浮点数),或在需要对值进行装箱(例如为其设置属性)时使用堆编号。

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

  • 虚拟机堆,或
  • 渲染器内存的外部。系统会创建一个封装容器对象,用于访问外部存储空间,例如,该存储空间中存储了脚本源和从 Web 接收的其他内容,而不是复制到虚拟机堆上。

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

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

Cons 字符串是一种由存储并联接的成对字符串组成的对象,是串联的结果。仅在需要时联接 cons 字符串内容。例如,需要构造已联接字符串的子字符串。

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

数组 - 数组是具有数字键的对象。它们在 V8 虚拟机中广泛使用,用于存储大量数据。用作字典的成套键值对由数组支持。

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

  • 命名属性
  • 数字元素

如果属性数量非常少,则可以将它们存储在 JavaScript 对象本身内部。

映射 - 用于描述对象种类及其布局的对象。例如,映射用于描述实现快速属性访问的隐式对象层次结构。

对象组

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

每个封装容器对象都会保留对相应原生对象的引用,用于将命令重定向到自身。相应对象组会自行持有封装容器对象。但是,这不会导致出现无法回收的循环,因为 GC 足够智能,可以释放封装容器不再被引用的对象组。但是,忘记释放单个封装容器将保留整个组和关联的封装容器。