GENEARE BY SD
Published on

V8中的对象

v8对对象属性的访问

排序属性和常规属性

在添加属性后,按照ecma的规范,对象的属性值按照如下的顺序排列

  1. 数字属性按照顺序
  2. 字符串属性按照添加顺序
数字属性被称为排序属性
字符串属性被称为常规属性 v8存储对象索引 在v8内部存在两个字段用于对这两种属性进行分类,其中element用于存储数字属性,properties用于存储字符串属性

快属性和慢属性

properties的数量小于等于十个的时候,v8会对索引进行优化,直接把字符串属性放在和properties统一层级的位置,以加快索引速度。称为对象内属性 对象内属性 对象内属性

通过chrome观察

  1. 当常规属性数量 = 10时 常规属性 = 10 只有一个elements属性。
  2. 当常规属性数量 = 20时 常规属性 = 20 出现了elements属性和properties属性,此时properties按线性排列,并且properties中保存的时从第11个开始的属性。
  3. 当常规属性数量 = 200时 常规属性 = 200 出现了elements属性和properties属性,此时properties按非线性排列
线性配列的属性被称为“快属性”,非线性排列属性的称为“慢属性” 慢属性存储

elements和数组索引

洞和打包元素

删除元素会产生hole,在属性访问的时候如果对象本身上不存在该属性则会往原型链上查找,而通过标记一个元素的值为the_hole可以简化查找过程,尤其是数组

快速elements和字典elements

针对属性描述符和包含大量空洞的数组使用字典的形式更加节省内存

const sparseArray = [];
sparseArray[9999] = 'foo';

const array = [];
Object.defineProperty(array, 0, {value: 'fixed' configurable: false});
console.log(array[0]);      // Prints 'fixed'.
array[0] = 'other value';   // Cannot override index 0.
console.log(array[0]);      //

整数和小数

对于只有整数的数组,v8会进行优化

const a1 = [1,   2, 3];  // Smi Packed
const a2 = [1,    , 3];  // Smi Holey, a2[1] reads from the prototype
const b1 = [1.1, 2, 3];  // Double Packed
const b2 = [1.1,  , 3];  // Double Holey, b2[1] reads from the prototype

隐藏类

JS作为动态语言,可以随时随地为一个对象增加一个属性(Map),在v8内部,为了对象的访问速度提出了隐藏类的概念。
当我们创建一个对象时,v8会为我们的对象分配一个隐藏类。隐藏类中记录了两点

  1. 有哪些属性
  2. 属性的偏移量 对象和隐藏类 通过隐藏类访问对象属性时,可以先得到某个属性相对于对象起始位置的偏移量,然后通过偏移量去访问相对应的值。 上面提到的快属性就会存在于隐藏类上,而慢属性不会。 慢属性的键、值、以及属性描述符记录在properties对比

复用隐藏类

当创建多个对象具有相同的属性名称、属性个数、以及顺序的时候,v8就会复用之前创建的隐藏类 复用隐藏类 同样的,当一个对象的属性被反复添加和删除时,隐藏类也会被重复创建和销毁

DescriptorArray

除了Map表示的隐藏类以外,还有两个属性共同构成了隐藏类DescriptorArrayTransitionArray。 这里先介绍DescriptorArrayDescriptorArray记录了Map中的属性,在记录时会按照添加顺序,同时还记录了关于属性的基本信息。 DescriptorArray 以这张图为例。 由于不断添加属性(图中nameheight等箭头上的属性)我们获得了Map0 - Map5 这五个隐藏类。同时还有三个DescriptorArray

其中Map1 - Map3指向了同一个DescriptorArray,而且每个Map也记录了在DescriptorArray中存放了多少了自己的属性。 这样在访问的时候,只需要访问自己的部分。对于Map1而言就只需要访问DescriptorArray中的第1个属性。

Map4Map5则共享了DescriptorArray2

TransitionArray

TransitionArray记录了属性->隐藏类的映射关系,也就是说我有一个类A,当我在A上添加属性时,生成的新的隐藏类会是哪一个。 TransitionArray TransitionArray用于追踪隐藏类的属性出现分叉的情况。比如在图中我们添加完height和width之后,我们再去添加experienceprominence时就是产生"分叉",因为这两个属性并不会同时存在,而是会产生分支结构。 通过TransitionArray我们可以快速定位具有不同的属性的对象所对应的隐藏类 TransitionArray

slack tracking

上面我们说到了对象的对象内属性,当属性被存放到properties的部分就称为backing store。 但是实际情况不会像上面那样简单,当使用到backing store时,10个的上限并不一定触发,这就涉及到了v8的slack tracking策略。

由于JS可以动态添加属性的策略,在为对象分配内存的时候通常会先分配一个较大的值,再根据实际的使用情况利用垃圾回收机制回收一部分空间。而追踪实际的使用情况的这部分被称为slack tracking。 当v8使用隐藏类构建对象时,会对构建的对象进行计数。

  1. 当一个新的隐藏类被创建的时候,它可以有10个对象内属性。当我们不断添加新的属性的之后,对象内属性可用的数量会不断减少。
  2. 后续通过隐藏类创建新的对象
  3. 创建的数量达到7个
  4. 对象内属性剩余可用的部分被清空,已有的对象内属性被保留,后续的新属性放入backing store中。
  5. slack tracking 完成
  6. 此时通过这个隐藏类再去创建对象,那么这个对象的大小将维持一个较小(相对slack tracking之前)的内存占用。

举例

> %DebugPrint(m1);
DebugPrint: 0x49fc866d: [JS_OBJECT_TYPE]
 - map: 0x58647385 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x49fc85e9 <Object map = 0x58647335>
 - elements: 0x28c821a1 <FixedArray[0]> [HOLEY_ELEMENTS]
 //此时两个属性
 - properties: 0x28c821a1 <FixedArray[0]> {
    0x28c846f9: [String] in ReadOnlySpace: #name: 0x5e412439 <String[10]: #Matterhorn> (const data field 0)
    0x5e412415: [String] in OldSpace: #height: 4478 (const data field 1)
 }
  0x58647385: [Map]
 - type: JS_OBJECT_TYPE
 //大小
 - instance size: 52
 //对象内属性
 - inobject properties: 10
 - elements kind: HOLEY_ELEMENTS
 //剩余的对象内属性
 - unused property fields: 8
 - enum length: invalid
 - stable_map
 - back pointer: 0x5864735d <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x5e4126fd <Cell value= 0>
 - instance descriptors (own) #2: 0x49fc8701 <DescriptorArray[2]>
 - prototype: 0x49fc85e9 <Object map = 0x58647335>
 - constructor: 0x5e4125ed <JSFunction Peak (sfi = 0x5e4124dd)>
 - dependent code: 0x28c8212d <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 6

当slack tracking 完成后

DebugPrint: 0x5cd08751: [JS_OBJECT_TYPE]
 - map: 0x4b387385 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x5cd086cd <Object map = 0x4b387335>
 - elements: 0x586421a1 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x586421a1 <FixedArray[0]> {
    0x586446f9: [String] in ReadOnlySpace: #name:
        0x51112439 <String[10]: #Matterhorn> (const data field 0)
    0x51112415: [String] in OldSpace: #height:
        4478 (const data field 1)
 }
0x4b387385: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 //对象内属性变为2
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 //剩余对象内属性变为0
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x4b38735d <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x511128dd <Cell value= 0>
 - instance descriptors (own) #2: 0x5cd087e5 <DescriptorArray[2]>
 - prototype: 0x5cd086cd <Object map = 0x4b387335>
 - constructor: 0x511127cd <JSFunction Peak (sfi = 0x511125f5)>
 - dependent code: 0x5864212d <

相关资料

  1. https://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html
  2. https://v8.dev/docs/hidden-classes
  3. https://v8.dev/blog/slack-tracking
  4. https://v8.dev/blog/fast-properties