本部分介绍了内存分析中的常用术语,适用于不同语言的各种内存分析工具。
此处所述的术语和概念适用于 Chrome 开发者工具堆分析器。如果您用过 Java、.NET 或其他内存分析器,那么本课程会对您有所帮助。
对象大小
可以将内存视为包含基元类型(如数字和字符串)和对象(关联数组)的图表。它可直观地表示为一个图表,其中包含多个互连的点,如下所示:
对象可以通过两种方式保留内存:
- 直接通过对象本身。
- 通过保留对其他对象的引用来隐式处理这些对象,从而阻止这些对象被垃圾回收器(简称 GC)自动处置。
使用开发者工具中的堆分析器(一种用于调查在“Profiles”下发现的内存问题的工具)时,您可能会发现自己查看的是几个不同的信息列。Shallow Size 和 Retained 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 字符串内容。例如,需要构造已联接字符串的子字符串。
例如,如果将 a 和 b 串联,则会得到表示串联结果的字符串 (a, b)。如果您稍后将 d 与该结果串联,则会得到另一个 cons 字符串 ((a, b), d)。
数组 - 数组是具有数字键的对象。它们在 V8 虚拟机中广泛使用,用于存储大量数据。用作字典的成套键值对由数组支持。
典型的 JavaScript 对象可以是用于存储数据的两种数组类型之一:
- 命名属性
- 数字元素
如果属性数量非常少,则可以将它们存储在 JavaScript 对象本身内部。
映射 - 用于描述对象种类及其布局的对象。例如,映射用于描述实现快速属性访问的隐式对象层次结构。
对象组
每个原生对象组都由保持对彼此的相互引用的对象组成。例如,在 DOM 子树中,每个节点都有一个指向其父级的链接,并链接到下一个子级和下一个同级,从而形成一个连通图。请注意,原生对象不会在 JavaScript 堆中表示,因此它们的大小为零。而是会创建封装容器对象。
每个封装容器对象都会保留对相应原生对象的引用,用于将命令重定向到自身。相应对象组会自行持有封装容器对象。但是,这不会导致出现无法回收的循环,因为 GC 足够智能,可以释放封装容器不再被引用的对象组。但是,忘记释放单个封装容器将保留整个组和关联的封装容器。