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代码,或者我们一起讨论下。
网友评论