内存术语

梅金·卡尼
Meggin Kearney

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

此处介绍的术语和概念是指 Chrome 开发者工具堆性能分析器。如果您使用过 Java、.NET 或其他某些内存分析器,不妨先回顾一下。

对象大小

将内存视为包含基元类型(如数字和字符串)和对象(关联数组)的图表。它在视觉上可以表示为由许多互连点的图表,如下所示:

内存的直观表示

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

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

使用开发者工具中的堆性能分析器(一种用于调查“Profiles”下所发现内存问题的工具)时,您可能会看到一些不同的信息列。浅层大小保留大小这两个选项较为突出,但它们分别代表什么?

浅层大小和保留大小

浅层大小

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

典型的 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 虚拟机或 VM)的内存相关主题。

JavaScript 对象表示法

有三种基元类型:

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

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

数字可以存储为:

  • 一个称为小整数 (SMI) 的 31 位直接整数值,或者
  • 堆对象,称为“堆数”。堆数字用于存储不适合 SMI 形式的值(例如“双精度”值),或者需要将值“装箱”时(例如在值上设置属性时)。

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

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

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

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

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

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

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

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

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

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

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

对象组

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

每个封装容器对象都会存储对相应原生对象的引用,用于将命令重定向到自身。这样,对象组会独立保存封装容器对象。但是,这不会造成不可回收的循环,因为 GC 非常智能,可以释放封装容器不再引用的对象组。但是,忘记释放单个封装容器将保留整个组和关联的封装容器。