了解内存术语

优质
小牛编辑
137浏览
2023-12-01

本节介绍内存分析中使用的常用术语,同样适用于其他不同语言的内存分析工具。

这里描述的术语和概念适用于Chrome DevTools堆分析器。如果你曾经使用过Java,.NET,或其他一些内存分析器,那么该篇的内容对你而言就是一次提升。

对象的大小

将内存想象为具有原始类型(如数字和字符串)和对象(关联数组)的图形。它可以在视觉上表示为具有多个相互关联的点图,如下:

对象可以通过两种方式驻留在内存中:

  • 直接通过对象本身。
  • 通过包含对其它对象的引用,这样就会阻止垃圾回收器(简称 GC)自动回收这些对象。

当使用DevTools中Heap Profiler(堆分析器)(Profiles(分析)面板下,一个用于查找的内存问题的工具)的时候,你会发现你所看到几个不同的信息列。其中最重要的就是Shallow Size(浅尺寸) 以及 Retained Size(保留尺寸),但这些代表什么呢?

Shallow Size(浅尺寸)

这是指对象本身持有的内存大小。

典型的 JavaScript 对象具有一些保留的内存,用于他们的描述以及存储直接值。通常情况下,只有数组和字符串才会有比较明显的Shallow Size(浅尺寸) 。不过,字符串和外部数组往往在渲染内存中有它们自己的主存储器,在 JavaScript 堆只暴露一个小包装器对象。

渲染内存是指被检查页面被渲染的过程中使用的内存:本地内存 + 该页面中 JS 堆内存 + 所有通过该页面启动的workers(这里指Service workers)使用 JS 堆的内存。然而,即使是一个小的对象,也可以通过阻止垃圾回收器自动回收其他对象,来间接占用大量的内存。

Retained size(保留尺寸)

这是指对象本身与其依赖对象一起被删除后所释放的内存大小,并且 GC roots 无法到达该处。

GC roots 是由在从原生代码的 V8 之外引用 JavaScript 对象的时候所创建的句柄(局部或者全局的)构成的。这些句柄可以在 GC roots > Handle scope 以及 GC roots > Global handles 堆快照中找到。在没有谈及浏览器实现的细节的情况下,就在本文中说明句柄会令读者感到困惑,故而关于句柄的细节本文不做讲解。事实上,GC roots 和 句柄,都不是你需要担心的东西。

有很多内部的GC roots,其中大部分对用户来说不感兴趣。 从应用程序的角度来说,有以下几种 roots:

  • Window 全局对象(在每一 iframe中)。在堆快照中,有一个距离域,其包含的是在Window最短保留路径上的属性引用的数目。
  • 文档 DOM 树由通过遍历文档可达到的所有本地DOM节点组成。 不是所有的节点都会有 JS 封装,但是如果他们有封装,那么只要文档还在,这些节点就可以使用。
  • 有些时候,对象会被 debugger(调试器)上下文以及 DevTools 控制台保留(例如,在控制台进行评估后)。建议创建堆快照前请先清空控制台和调试器中没用的断点。

下面的内存图就是从一个根节点开始的,这个根节点可能是浏览器的 window 对象或者是 Node.js 模块的 Global 对象。你不能控制这个根对象是如何被回收的。

任何无法从根节点到达的元素够将被回收。

Note: Shallow 和 Retained size 都用字节为单位来表示数据。

对象的保留树

就像我们前面所说的,堆就是由相互连接的对象构成的网络。在数学的世界中,这种结构称作图形或者内存图。图形由通过边缘连接的节点构成,其中节点和边都有相应的标签。

  • 节点(或者对象) 是用用于构建它们的构造函数的名称来标记的。
  • 是用属性名来标记的。

学习如何使用堆分析器来录制资料。在堆分析器记录中我们可以看到包括 Distance 在内的几栏:Distance 指的是从根节点到当前节点的距离。有一种情况是值得探究的,那就是几乎所有同类的对象都有着相同的距离,但是有一小部分对象的 Distance 的值要比其他对象大一些。

在下面的Heap Profiler录制中我们可以看到一些东西(红框高亮显示的),包括distance(距离)列:从GC roots到当前节点的距离。 有一些值得探究的东西,如果几乎所有相同类型的对象处于相同的距离,但是有一小部分处于更大的距离。

4.png

Dominators(支配者)

支配者对象是由树形结构组成的,因为每个对象都刚好有一个支配者。一个对象的支配者不一定直接引用它所支配的对象,也就是说,支配树并不是图的生成树。

在下面的图中:

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

5.png

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

6.gif

V8 的细节

当分析内存时,理解堆快照为什么是看到的那个样子,是很有帮助。本节介绍一些与V8 JavaScript虚拟机(V8 VM或VM)对应的内存相关话题。

JS对象的表现形式

JavaScript 中有三种主要类型:

  • Numbers(数字)(比如,3.14159..)
  • Booleans(布尔值)(true 或者 false)
  • Strings(字符串) (比如 “Werner Heisenberg”)

这些类型在树中都是叶子节点或者终结节点,并且它们不能引用其它值。

Numbers(数字) 可以像下面这样存储:

  • 相邻的 31 位整数值,被称为 small integers (SMIs)
  • 堆对象,被引用为heap numbers(堆数字)。堆数字用于存储不适合 SMI 形式的值,比如浮点类型(doubles),或者是需要封装(boxed)的值,比如设置其属性值的类型。

Strings(字符串) 可以被存储在:

  • VM heap(虚拟机的堆)中,或者
  • 外部的renderer’s memory(渲染内存)。也就是当创建或者使用一个封装后的对象时需要使用的外部存储器,比如,脚本资源以及其他从网上接收而不是赋值到虚拟机堆中存储的内容。

新的 JavaScript 对象的内存是由特定的 JavaScript 堆(或者说 VM heap)分配的。这些对象由 V8 垃圾回收器管理,并且只要存在一个对他们的强引用就不会被回收。

Native objects(本地对象) 指的是不在 JavaScript 堆中存储的一切对象。本地对象和堆对象相反,其生存周期不由 V8 垃圾回收器管理,并且只能通过封装它们的 JavaScript 对象来使用。

Cons string 是一个保存了成对字符串的对象,并且该对象会将字符串拼接起来,最后的结果是串联后的字符串。拼接后的 cons string 的内容只有在需要的时候才会出现。一个比较好的例子就是,如果想获取某个字符串的子串,就必须利用函数进行构建。

举个例子,如果你将 ab 对象串联,那么你将获得一个字符串(a,b) 用于表示拼接后的结果。如果你之后又加入了一个对象 d,那么你将活的另一个字符串((a,b),d)。

Arrays(数组) - 一个数组就是有着数字键的对象。他们广泛应用在 V8 VM 中,用于存储大量数据。在字典这样的数据结构中键值对的集合就是利用数组来备份的。

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

  • 命名的属性
  • 数字元素

如果想要存储的是少量的属性,那么它们可以直接在 JavaScript 对象中存储。

Map(映射) - 一个对象,用于描述对象及其布局的种类。举个例子,maps 用于描述快速属性访问的隐式对象结构。

对象组

每个本地的对象组都是由保持彼此相互引用的对象组成的。以一个 DOM 子树为例,在该树中,每一个节点都一个指向父节点的连接,以及指向孩子节点和兄弟节点的链接,由此,所有的节点连成了一张图。需要注意的是,本地对象并不会在 JavaScript 堆中出席那,所以它们的大小是 0。相应的,对于每个要使用本地对象都会创建一个对应的封装对象。

每个封装对象都含有一个对相应的本地对象的引用,这是为了能够将命令重定向到本地对象上。而对象组则含有这些封装的对象,但是,这并不会造成一个无法回收的死循环,因为垃圾回收器会自动释放不在引用的封装对象。但是一旦忘记了释放某个封装对象就可能造成整个组以及相关封装对象都无法被释放。