本部分介绍了内存分析中常用的术语,适用于适用于不同语言的各种内存性能分析工具。
本文中介绍的术语和概念均与 Chrome 开发者工具堆性能分析器相关。如果您曾使用过 Java、.NET 或其他内存性能分析器,则可以将此部分作为复习内容。
对象大小
将内存视为包含基元类型(例如数字和字符串)和对象(关联数组)的图。它在视觉上可以表示为包含多个相互关联的点的图表,如下所示:
对象可以通过以下两种方式占用内存:
- 直接由对象本身进行调用。
- 隐式地保留对其他对象的引用,从而防止这些对象被垃圾回收器(简称 GC)自动处置。
在 DevTools 中使用堆分析器(用于调查内存面板中发现的内存问题的工具)时,您可能会看到几个不同的信息列。其中最引人注目的是浅层大小和保留大小,但它们分别代表什么?
浅层大小
这是对象本身占用的内存大小。
典型的 JavaScript 对象会预留一些内存来存储其说明和立即值。通常,只有数组和字符串可以具有较大的浅层大小。不过,字符串和外部数组的主要存储空间通常位于渲染程序内存中,并且只在 JavaScript 堆上公开一个小型封装容器对象。
渲染程序内存是指用于渲染受检页面的进程的所有内存:原生内存 + 页面的 JS 堆内存 + 由页面启动的所有专用工作器的 JS 堆内存。不过,即使是小对象,也可以通过阻止自动垃圾回收进程处置其他对象,间接占用大量内存。
保留的大小
这是在删除对象本身及其无法从 GC 根访问的依赖对象后释放的内存大小。
GC 根由句柄组成,这些句柄是在从原生代码引用 V8 之外的 JavaScript 对象时创建的(本地或全局)。您可以在堆快照的 GC 根 > Handle 作用域和 GC 根 > 全局句柄下找到所有此类句柄。如果本文档中仅介绍了句柄,而没有深入探讨浏览器实现的细节,可能会造成混淆。您无需担心 GC 根和句柄。
存在许多内部 GC 根,其中大多数对用户而言并不重要。从应用的角度来看,根有以下几种:
- 窗口全局对象(在每个 iframe 中)。堆快照中有一个距离字段,该字段是从窗口到最短保留路径上的属性引用数量。
- 文档 DOM 树,由遍历文档可访问的所有原生 DOM 节点组成。其中有些可能没有 JS 封装容器,但如果有,则封装容器会在文档有效期间保持有效。
- 有时,调试程序上下文和开发者工具控制台可能会保留对象(例如在控制台评估后)。创建堆快照,并确保控制台清晰且调试器中没有有效断点。
内存图从根开始,该根可以是浏览器的 window
对象,也可以是 Node.js 模块的 Global
对象。您无法控制此根对象的 GC 方式。
从根无法访问的任何内容都会被 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 形式的值(例如双精度值),或者在需要对值进行封装时(例如在对其设置属性时)。
字符串可以存储在以下任一位置:
- 虚拟机堆,或者
- 在渲染程序的内存中进行外部处理。系统会创建一个封装容器对象,用于访问外部存储空间,例如存储脚本源代码和从 Web 收到的其他内容,而不是将其复制到虚拟机堆上。
系统会从专用 JavaScript 堆(或 VM 堆)分配新的 JavaScript 对象的内存。这些对象由 V8 的垃圾回收器管理,因此只要至少有一个对它们的强引用,它们就会保持活跃状态。
原生对象是指不在 JavaScript 堆中的所有其他对象。与堆对象不同,原生对象在其整个生命周期内都不会由 V8 垃圾回收器管理,并且只能通过其 JavaScript 封装容器对象从 JavaScript 访问。
Cons 字符串是一种对象,由存储后联接的字符串对组成,是串联的结果。仅在需要时才会联接 cons 字符串内容。例如,需要构建联接字符串的子字符串时。
例如,如果您串联 a 和 b,则会得到字符串 (a, b),表示串联的结果。如果您稍后将 d 与该结果串联,则会得到另一个 cons 字符串 ((a, b), d)。
数组 - 数组是具有数字键的对象。它们在 V8 虚拟机中广泛用于存储大量数据。像字典一样使用的键值对集由数组进行备份。
典型的 JavaScript 对象可以是用于存储以下两种数组类型之一:
- 命名属性,以及
- 数字元素
如果属性数量非常少,则可以将其存储在 JavaScript 对象本身的内部。
Map:用于描述对象类型及其布局的对象。例如,映射用于描述隐式对象层次结构,以实现快速属性访问。
对象组
每个原生对象组都由彼此相互引用的对象组成。例如,假设有一个 DOM 子树,其中每个节点都有指向其父节点的链接,以及指向下一个子节点和下一个同级兄弟节点的链接,从而形成一个连通的图。请注意,JavaScript 堆中不包含原生对象,因此它们的大小为零。而是会创建封装容器对象。
每个封装容器对象都包含对相应原生对象的引用,以便将命令重定向到该对象。而对象组则包含封装容器对象。不过,这不会产生不可收集的循环,因为 GC 足够智能,可以释放不再引用封装容器的对象组。但是,如果忘记发布单个封装容器,则会导致整个组和关联的封装容器都无法发布。