美文网首页
V8中的JS数据类型

V8中的JS数据类型

作者: JunhYan | 来源:发表于2021-01-06 15:47 被阅读0次

    ECMA-262中定义ECMAScript的类型:
    Undefined, Null, Boolean, String, Symbol, Number, BigInt, Object
    这个是大忌都比较熟悉的,各个类型在V8中是怎么一种表达形式,是本文要介绍的

    数据类型继承关系图

    图-1

    图-1罗列了出了与本文有关的一些类的继承关系,Primitiveheapobject从名字(原始堆对象)也可以看出来,其实是真正存储原始数据的类。jsReceiver的实例中存储的基本上都是指针。本文主要介绍 Object, Smi,JSObject,HeapNumber, String, Oddball。

    Object(V8 Object)

    对于所有的js数据类型,都是由一个MaybeObject接收,为什么是MaybeObject而不是Object,这里说明下他们的区别
    Object :
    --- Smi
    --- A strong reference to a HeapObject
    MaybeObject :
    --- Smi
    --- A strong reference to a HeapObject
    --- A weak reference to a HeapObject
    --- A cleared weak reference.
    什么是强引用?强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
    什么是弱应用?对于只具有弱引用的对象拥有短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。可以理解为一种待回收的状态。在我们实际开发中用到的很少。
    只有确认当前数据类型的实例引用是强类型的时候才从MaybeObject类型转换成Object类型。
    MaybeObject和Object继承了TaggedImpl,TaggedImpl有且只有一个数据成员变量,就是ptr_:
    StorageType ptr_;
    对于不同引用类型或者Smi(小整数,31位,最后一位用来做类型区分)的区分如下:
    Smi: ...xxxxxxx0
    Strong: ...xxxxxx01
    Weak: ...xxxxxx11 (where ...xxxxxx != nullptr)
    Cleared weak: ...00000011

    JSObject(JS Object)

    1、概述
    图-2

    图-2就是一个JS对象的实例,主要有两部分组成,header 和 body,body里面放的是对象的properties,这里的properties是inobject properties,那肯定就有outobject properties,后面介绍。上面标的kMaxInstanceSize就是这个实体的最大限制。kTaggedSize是一个指针的大小,64位指针是8个字节64位,32位是4个字节32位。kHeaderSize是头的大小(3个指针),这三个指针就是map properties elements指针,具体作用在后面一一介绍,kMaxInObjectProperties是最大的inobject properties数量,kTaggedSizeLog2 对应指针的字节数:8 或者4。

    2、实例化一个空对象
    var obj = {};
    
    图-3

    对于一个空对象会创建一个8 * kTaggedSize字节的实例,会预留四个空的slot给inobject_properties。
    比如插入如下代码

    obj.a = 'aaa';
    obj.b = 'bbb';
    obj.c = 'ccc';
    

    如图-4会在空对象的inobject_properties中分别增加a、b、c指针。


    图-4

    当继续插入property,比如增加如下属性:

    obj.d = 'ddd';
    obj.e = 'eee';
    obj.y = 'yyy';
    
    图-5

    如图-5所示,d指针会被加入到inobject_properties,当4个slot都被占用后,不会在JSObject实体里面开辟新空间,而是创建一个PropertyArray,PropertyArray默认开辟3个指针空间来存放outobject properties,但这个属性确实又是属于当前JSObject实体的,所以JSObject实体的properties指针指向新创建的PropertyArray。
    当我们继续往JSObject实体里添加属性时候,V8里面会做以下处理:
    比如继续添加outobject properties:m和n

    obj.m = 'mmm';
    obj.n = 'nnn';
    
    图-6
    图-7

    整个过程如图-6和图-7所示会执行以下流程
    1、index.outobject_array_index()与length比较,index是property应该插入的位置,当插入m的时候index是2,length是3,正常插入。当插入n的时候,当前PropertyArray空间不足了,调用CopyPropertyArrayAndGrow方法
    2、计算grow_by,grow_by是需要增加的长度,在当前情形下会使用默认值3
    3、根据new_len创建一个新的FixedArray
    4、依据旧数组的map设置新数组的map
    5、转换FixedArray为PropertyArray
    6、更新length字段
    7、采用memcpy复制原数组数据
    8、设置可插入slot位置
    9、在该位置插入n
    PropertyArray的长度也不是无止境增加的,它的长度最大值是kMaxInt(int类型的最大值)
    elements指针是用来干什么的?
    同样用上面的对象做例子

    var obj = {};
    obj[0] = 'aaa';
    obj[1] = 'bbb';
    

    这段代码在V8中会表达成如图-8所示


    图-8

    elements指向的是对象的数字属性所在的实体。

    3、实例化一个带属性对象
    var obj = {x: 'hello', y: 'world'};
    
    图-9

    对于上述代码,在创建对象实例的时候会根据属性的数量创建与之对对应的slot,此时只分配指针空间,但不做具体的指针插入。具体把x,y指针插入inobject_properties是在代码运行的时候,在runtime的时候调用函数
    CreateLiteral
    CreateLiteralWithoutAllocationSite
    CreateObjectLiteral
    来创建字面量对象,创建出来的最终结构如图-9所示。
    再为该对象新增属性时会加入到outobject_properties中,如图-10


    图-10
    4、Map

    map指针会指向一个Map的实体,Map主要是表述在堆内部存储对象的结构,主要描述对象的大小,类型等;同时Map为GC提供了遍历对象的方式。具体的Map如图-10:


    图-11

    实体的具体描述,在这里应用一下V8里的注释

    // Map layout:
    // +---------------+------------------------------------------------+
    // |   _ Type _    | _ Description _                                |
    // +---------------+------------------------------------------------+
    // | TaggedPointer | map - Always a pointer to the MetaMap root     |
    // +---------------+------------------------------------------------+
    // | Int           | The first int field                            |
    //  `---+----------+------------------------------------------------+
    //      | Byte     | [instance_size]                                |
    //      +----------+------------------------------------------------+
    //      | Byte     | If Map for a primitive type:                   |
    //      |          |   native context index for constructor fn      |
    //      |          | If Map for an Object type:                     |
    //      |          |   inobject properties start offset in words    |
    //      +----------+------------------------------------------------+
    //      | Byte     | [used_or_unused_instance_size_in_words]        |
    //      |          | For JSObject in fast mode this byte encodes    |
    //      |          | the size of the object that includes only      |
    //      |          | the used property fields or the slack size     |
    //      |          | in properties backing store.                   |
    //      +----------+------------------------------------------------+
    //      | Byte     | [visitor_id]                                   |
    // +----+----------+------------------------------------------------+
    // | Int           | The second int field                           |
    //  `---+----------+------------------------------------------------+
    //      | Short    | [instance_type]                                |
    //      +----------+------------------------------------------------+
    //      | Byte     | [bit_field]                                    |
    //      |          |   - has_non_instance_prototype (bit 0)         |
    //      |          |   - is_callable (bit 1)                        |
    //      |          |   - has_named_interceptor (bit 2)              |
    //      |          |   - has_indexed_interceptor (bit 3)            |
    //      |          |   - is_undetectable (bit 4)                    |
    //      |          |   - is_access_check_needed (bit 5)             |
    //      |          |   - is_constructor (bit 6)                     |
    //      |          |   - has_prototype_slot (bit 7)                 |
    //      +----------+------------------------------------------------+
    //      | Byte     | [bit_field2]                                   |
    //      |          |   - new_target_is_base (bit 0)                 |
    //      |          |   - is_immutable_proto (bit 1)                 |
    //      |          |   - unused bit (bit 2)                         |
    //      |          |   - elements_kind (bits 3..7)                  |
    // +----+----------+------------------------------------------------+
    // | Int           | [bit_field3]                                   |
    // |               |   - enum_length (bit 0..9)                     |
    // |               |   - number_of_own_descriptors (bit 10..19)     |
    // |               |   - is_prototype_map (bit 20)                  |
    // |               |   - is_dictionary_map (bit 21)                 |
    // |               |   - owns_descriptors (bit 22)                  |
    // |               |   - is_in_retained_map_list (bit 23)           |
    // |               |   - is_deprecated (bit 24)                     |
    // |               |   - is_unstable (bit 25)                       |
    // |               |   - is_migration_target (bit 26)               |
    // |               |   - is_extensible (bit 28)                     |
    // |               |   - may_have_interesting_symbols (bit 28)      |
    // |               |   - construction_counter (bit 29..31)          |
    // |               |                                                |
    // +****************************************************************+
    // | Int           | On systems with 64bit pointer types, there     |
    // |               | is an unused 32bits after bit_field3           |
    // +****************************************************************+
    // | TaggedPointer | [prototype]                                    |
    // +---------------+------------------------------------------------+
    // | TaggedPointer | [constructor_or_backpointer_or_native_context] |
    // +---------------+------------------------------------------------+
    // | TaggedPointer | [instance_descriptors]                         |
    // +****************************************************************+
    // ! TaggedPointer ! [layout_descriptors]                           !
    // !               ! Field is only present if compile-time flag     !
    // !               ! FLAG_unbox_double_fields is enabled            !
    // !               ! (basically on 64 bit architectures)            !
    // +****************************************************************+
    // | TaggedPointer | [dependent_code]                               |
    // +---------------+------------------------------------------------+
    // | TaggedPointer | [prototype_validity_cell]                      |
    // +---------------+------------------------------------------------+
    // | TaggedPointer | If Map is a prototype map:                     |
    // |               |   [prototype_info]                             |
    // |               | Else:                                          |
    // |               |   [raw_transitions]                            |
    // +---------------+------------------------------------------------+
    

    instance_descriptors(实体描述指针)会指向一个DescriptorArray的实体,DescriptorArray是对于Fast Mode(在V8中对象分为Fast Mode(快速模式)和Dictionary Mode(字典模式))的属性的描述,图-10的对象实体在DescriptorArray中的具体表现如图-12所示:


    图-12

    在Fast Mode下,可以快速的定位到属性在对象中的具体位置。对于一般来说新创建的属性较少的对象都是Fast Mode对象,但出现以下几种操作后,对象会从Fast Mode转为Dictionary Mode:
    1、动态添加过多的属性
    2、删除非最后添加的属性
    3、对象被当作哈希表使用时(如存储大量数据)

    Smi和HeapNumber(Number)

    在V8中数字其实目前有三种表示方式:Smi、HeapNumber和BigInt,BigInt是ecma新引入的一个概念,在本文中先不对其进行介绍。

    Smi

    V8中的小整数,直接放到ptr_指针中做存储,会放到的V8的栈内存存储。具体存储结构如下:


    图-13

    如图-13所示一个符号位,最后一位是ptr_分类使用的字段,其他是数字位,之所以叫小整数是因为比整数少了一位。

    HeapNumber

    不考虑BigInt,V8中超过Smi的数字和浮点数都是通过HeapNumber来存储的,HeapNumber是存储在V8的堆内存中的,具体存储结构是依据IEEE的标准的。


    图-14

    对于浮点数或者大的整数,具体的计算方式如下:


    数字类型的表达还是比较简单的。BigInt作为新的类型在之后酌情增加到这个模块中。

    String

    字符串表现形式

    在V8中字符串的展现形式主要就以下5种:
    1、SeqString连续空间存储的字符串,SeqString又表现为两种SeqOneByteString单字节连续字符串和SeqTwoByteString双字节连续字符串,SeqOneByteString主要是用来存储ASCLL编码如英文数字等单字节字符的,同理SeqTwoByteString是用来存储如汉字等双字节编码字符的。SeqString是真正的存储字符串的内存。(与JVM不同,JVM对于子字符串和拼接字符串都会创建新的存储,是JVM1.7-1.8的一个优化,主要原因还是在于GC的优化处理,目前V8的方式与JVM之前版本类似,保留了StringTable等)
    2、ConsString树形结构的字符串表示,多用于字符串拼接主要由两个指针组成:first指针和second指针,分别指向拼接字符串的两项。
    3、ThinString引用另一个字符串对象,这个其实是一个特殊的ConsString,是只有一个指针的ConsString,actual指针指向实际的字符串存储。
    4、SlicedString子字符串,在目前的V8当中子字符串是通过parent,offset和[length]来表达的,parent会指向该子字符串的父字符串,offset是子字符串在父字符串中的偏移,length是一个可选的字段,子字符串就是父字符串从offset到offset + length的字符串,length如果没有,就是到父字符串的最末端。
    5、ExternalString没有在V8堆里存储的字符串,比如V8内部使用的字符串等。
    下面用实际实例来对字符串做进一步的介绍

    var a = 'hello';
    var b = 'world';
    var c = a + b;
    

    上面代码在V8中会以图-15的方式表达


    图-15

    字符串a和字符串b都会存储在SeqOneByteString中,字符串c作为一个拼接的字符串为什么也会被存储在SeqOneByteString中呢?是因为对于ConsString形式拼接的字符串,其最小长度是ConsString::kMinLength = 13,如果拼接字符串长度大于13,那拼接后的字符串就会以图-16的方式表达

    var d = 'haha';
    var e = c + d;
    
    图-16
    StringTable与字符串常量

    StringTable是常量字符串表,以哈希表的方式存储了常量字符串的key和字符串存储在V8内存堆中的地址,一个常量字符串在V8中的表现形式如图-17所示


    图-17

    目前常量字符串仍旧是存储在SeqString中,区别于普通的字符串在于其map中的instance_type会标识为kInernalizedTag。在查找常量字符串的时候会通过字符串的key在StringTable中找到对应的字符串地址。
    对于创建或者这查找常量字符串在V8中一个很关键的函数是LookupKey (Isolate* isolate, StringTableKey* key) ,第一个参数是V8中的调度参数,主要的参数是第二个key。整个函数简单概况就是为了在StringTable查找常量字符串,如果当前字符串查找不到就在StringTable中创建该字符串的entry。LookupKey函数具体流程如图-18


    图-18
    在这个流程整个是一个轮询的过程。其他部分都比较好理解,先查询字符串,如果没有找到字符串,则需要在StringTable中新插入了,这个时候要判断下当前的StringTable有没有能力去支持新的常量字符串entry加入,在判断了StringTable的能力后,StringTable当前能力可能不能支持直接插入,则会去创建一个新的maybe_new_table。
    有点复杂的是在创建maybe_new_table后。因为对于StringTable的查询是可以并行的,为什么叫maybe_new_table,是因为它可能会作为一个新的StringTable,也可能会被直接释放掉。在当前线程获得锁后,会去重新加载StringTable(reloaded_table),如果reloaded_table不是最初的我们拿到的table,那就使用最新的reloaded_table,释放我们自己创建的maybe_new_table。如果还是之前的table,那就要把maybe_new_table正式转成new_table,但需要判断new_table的能力,因为在拿到锁之前可能有其他线程往对原table做了操作,如果能力不够则退出当前循环,继续之前的操作。如果能力够,则使用new_table,并且把原table的内容都映射到new_table中。

    table被重新赋值后需要重新判断一下当前table的能力,new_table能力正常没问题,但是对于reload_table可能不一定满足再向里面插入一个字段的能力。如果能力够,再插入前判断下是不是当前的key在table中已经存在了,可能别的线程已经插入了相同的常量字符串。如果没有,则把该字符串常量对应的key和entry插入到StringTable中。返回新插入的字符串。整个流程结束。
    StringTable中的key是怎么生成的呢?在V8中对于字符串长度length <= String::kMaxHashCalcLength(16383)时采用的Jenkins One At A Time Hash的方式生成的key,具体算法,大家可以通过很多方式查询到。对于字符串长度大于16383的是根据字符串长度做的hash,具体如下

    uint32_t hash = static_cast<uint32_t>(length);
    return (hash << String::kHashShift) | String::kIsNotIntegerIndexMask;
    

    根据字符串长多做hash这中方式会产生很多的哈希冲突,所以在开发过程中尽量避免太长的常量字符串。

    Oddball(null,undefined,true,false)

    对于这几个类型,本文主要就介绍下他们的初始化方法。

    void Oddball::Initialize(Isolate* isolate, Handle<Oddball> oddball,
                             const char* to_string, Handle<Object> to_number,
                             const char* type_of, byte kind) {
      Handle<String> internalized_to_string =
          isolate->factory()->InternalizeUtf8String(to_string);
      Handle<String> internalized_type_of =
          isolate->factory()->InternalizeUtf8String(type_of);
      if (to_number->IsHeapNumber()) {
        oddball->set_to_number_raw_as_bits(
            Handle<HeapNumber>::cast(to_number)->value_as_bits());
      } else {
        oddball->set_to_number_raw(to_number->Number());
      }
      oddball->set_to_number(*to_number);
      oddball->set_to_string(*internalized_to_string);
      oddball->set_type_of(*internalized_type_of);
      oddball->set_kind(kind);
    }
    

    方法内容有兴趣的可以研究下,其实主要看形参命名就可以知道具体要实现的功能。
    oddball是数据类型的引用,表示在V8中怎么调用相关类型。
    to_string是该oddball类型强转为String类型后的展示
    to_number是该oddball类型强转为Number后的展示
    type_of是typeof该oddball类型后的展示
    kind表示oddball的类型

    null
      Oddball::Initialize(isolate(), factory->null_value(), "null",
                          handle(Smi::zero(), isolate()), "object", Oddball::kNull);
    
    undefined
    Oddball::Initialize(isolate(), factory->undefined_value(), "undefined",
                          factory->nan_value(), "undefined", Oddball::kUndefined);
    
    true
     Oddball::Initialize(isolate(), factory->true_value(), "true",
                          handle(Smi::FromInt(1), isolate()), "boolean",
                          Oddball::kTrue);
    
    false
    Oddball::Initialize(isolate(), factory->false_value(), "false",
                          handle(Smi::zero(), isolate()), "boolean",
                          Oddball::kFalse);
    

    总结

    关于JS数据类型在V8中的表达本文做了相对全面的介绍,也是为笔者阅读V8代码做一个阶段总结。文章中只介绍了几个关键的数据类型,有些新类型比如symbol,bigint都没有过多的介绍。一个原因是因为本身在实际开发中应用的比较少,另一个是笔者还没有仔细看比如bigint相关代码。
    文章中很多的地方都是只告诉了大家执行的结果,很多细节的部分过程性的内容,如果有兴趣的可以自己阅读下V8代码,或者我们一起讨论下。

    相关文章

      网友评论

          本文标题:V8中的JS数据类型

          本文链接:https://www.haomeiwen.com/subject/qlyvoktx.html