奇技淫巧学 V8 之二,对象在 V8 内的表达

奇技淫巧学 V8 之二,对象在 V8 内的表达

先划重点:V8 中具有相同构建结构的 JSObject 对象,具有相同的内存(空间)布局。


JavaScript 对象会在堆上(根据需求)分配恒定大小的空间:

  • 预分配(不超过)一定大小*的空间用作对象内属性存储(inobject_properties)。
  • 预分配空间不足时(无空闲 slot),新增属性会存储在 properties 内。
  • 数字式属性存储在 elements 内。
  • properties/elements 空间不足时会创建(拷贝)一个更大的 FixedArray。
*
V8 定义 JSObject 最大实例大小为 kMaxInstanceSize = 2040 byte,
其中:
kPointerSize = sizeof(void *) = 8 byte(64 位平台),
kHeaderSize = kPointerSize * 3,
所以:max(inobject_properties) = 252 个

为了便于说明,我们来看个例子:

1、新建一个对象

var obj = {};

通过对象字面量创建的无属性对象分配 4 个对象内属性存储(inobject_properties)空间。


2、添加 3 个属性

obj.x = 1;
obj.y = 2;
obj.z = 3;


属性("x", "y", "z")优先存储在对象内属性存储中。


3、添加 2 个数字式属性

obj[0] = "a";
obj[1] = "b";

数字式属性("0", "1")存储在 elements 内。


4、继续添加 3 个属性

obj.a = "a";
obj.b = "b";
obj.c = "c";

优先使用对象内属性存储("a"),预分配空间不足时,属性("b", "c")会存储在 properties 内。


看到这里问题来了,V8 是如何知道空间分配与空间结构的状况了?

这就要提起 <Map> 了,每个在堆内创建的实例均有一个描述其结构的 <Map> 。

我们以 JSObject<Map> 为例:

  • type: 表述了堆内实例是一个 JSObject 对象
  • inobject properties:对象内存储空间(包含未使用的 slots)
  • unused property fields:未使用的属性存储空间
  • instance size:实例(在堆内)的大小
  • constructor:对象构造器
  • prototype:对象原型
  • stable[dictionary]:对象当前状态
    • stable_map:快速模式
    • dictionary_map:字典模式
  • … …

当对象处于字典模式时直接通过 Jenkins hash 则可得到属性值存取的位置,对象处于快速模式时 V8 采用 JSObject<Map> 中 instance_descriptors 标识对象实例的属性名与其值的存取位置(如下图所示):


除此之外,处于快速模式的对象 V8 还结合 JSObject<Map> 中 raw_transitions 提升对象实例的存取访问性能。

为了便于理解 Transitions,我们来举个例子:

在实例化构造函数时,根据 ECMA-262 标准,新建一个空对象:


在添加属性 x = 1 时:

  1. 生成新的 <Map>(#1) 并将 JSObject<Map> 的引用改为新创建的 <Map>(#1)。
  2. 向原先引用的 <Map>(#0) 的 transitions 中写入一个 transition #x to Map(#1)。
  3. 在当前 instance_descriptors(own) 写入 #x in offset 0。
  4. 将数字 1(Smi) 写入 JSObject 对象内属性存储的第 0 位(根据 instance_descriptors 中的标识)。

形成如下图结构:


继续添加属性 y = 2 时,执行过程与添加属性 x 类似,最终生成的对象 p1 如下图结构:

一般来说通过对象的构建过程,最终会形成 TransitionsList 甚至是 TransitionsTree

但是看上去在对象 p1 实例化过程中各个 <Map> 的 Transitions 并没有什么用?事实也的确实如此!喵喵喵?!但如果再次实例化对象就可以看到它的用处了。


我们再次实例化构造函数,但这回传入的参数为两个字符串("a", "b"):

在添加属性 x = "a" 时:

  1. 在 JSObject<Map>(#0) 的 transitions 中根据 key=x 查找 transition 记录,并按图索骥将 JSObject<Map> 的引用改为发现的 <Map>(#1) (在这里并不会再新建 <Map>)。
  2. 读取当前的 instance_descriptors 查找到 #x 的存储位置 offset 0。
  3. 将字符串 "a" (Pointer)写入 JSObject 对象内属性存储的第 0 位(根据 instance_descriptors 中的标识)。
需要注意的是:这里的 DescriptorArray 实例(也就是 instance_descriptors )为 <Map>(#2) 所有,并非 <Map>(#1)(#0) ,故在这里 instance_descriptors 只能读取不可写入 。

形成如下图结构:


继续添加属性 y = "b" 时,执行过程与添加属性 x 类似,最终生成的对象 p2 如下图结构:

在这里我们发现,虽然 p1与 p2 存储的实际数据(类型)不同,但是通过 Transitions 特性它们共享相同的一个 <Map>(#2),这也就导致他们同时也共享一个 instance_descriptors,所以它们的空间结构(布局)却是相同的(如下图所示)。


我们总结下,一般来说由相同顺序、相同属性名(同一构造函数)构建的对象,共享同一个 <Map>。

JSObject<Map> 也被称为 Hidden Class,这是由于最早 V8 在 Design Elements 将其称之为 Hidden Class,故一直沿用至今。

也就说,在 V8 中具有相同构建结构的 JSObject 对象,在堆内具有相同的内存(空间)布局

而相同的内存(空间)布局是 V8 优化对象存取的基础。

至于 V8 是如何基于此基础进行存取优化的了?欢迎收看下一章:

编辑于 2017-08-31