《Chrome V8原理讲解》第十七篇 JS对象的内存布局与创建过程

阅读量145460

发布时间 : 2021-11-01 10:00:06

 

前言

本系列的前十三篇文章,讲解了V8执行Javascript时最基础的工作流程和原理,包括词法分析、语法分析、字节码生成、Builtins方法、ignition执行单元,等等,达到了从零做起,入门学习的目的。
接下来的文章将以问题为导向讲解V8源码,例如:以闭包技术、或垃圾回收(GC)为专题讲解V8中的相关源码。V8代码过于庞大,以问题为导向可以使得学习主题更加明确、效果更好。同时,我争取做到每篇文章是一个独立的知识点,方便大家阅读。
读者可以把想学的内容在文末评论区留言,我汇总后出专题文章。

 

1 摘要

《javascript高级程序设计》中对JS对象有这样的描述:“ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。可以把ECMAScript的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。”v8官方文档提到这样的描述:“出于性能、亦或代码设计的考虑,V8中对数据类型的设计做了详细分类,对JS对象内部的数据成员也做了不同的设计”。
本文深入V8内部,详细剖析JS对象的创建过程,讲解JS对象内部成员的组成方式、内存布局,以及重要数据结构。本文内容组织方式:V8中JS对象的重要概念、成员组成和内存布局(章节2);JS对象创建过程(章节3)。

 

2 JS对象

在V8中,JS对象的每个成员、方法都有详细的分类和内存组织规则,内部成员从数据类型看分为两大类,元素类(Element)和属性类(Property),如图1。

图1(来自V8官方)中,可以看到元素和属性分开存储,原因是为提高效率。Element成员可以利用下标访问,它存在连续的地址空间中。Property成员,也是存在连的地址空间中,但不能使用下标访问成员,需要借助Map(HiddenClass)访问,Map记载了数据的描述符,通俗地说,Map描述数据的形状,数据访问方式等,参见第十四篇文章。元素类数据不需要借助Map,他的访问效率要高一些,如图2。

图2(出处同图1)中,除了Element和Property之外,还有In-object property,它与前面提到的Property不同之处是访问时不需要借助Map,这提高了访问效率,但In-object property的数量有限,优先使用In-object property,用完之后使用前面提到的“正常”Property进行存储。

图3(出处同图1),JS对象成员的访问方式有三种:
(1) In-Object,JS对象负责维护地址,它直接存在JS对象中;
(2) Slow方式,需要借助Map访问其成员;
(3) Self-dict,JS对象自己维护地址,不需要借助Map,采用字典方式存储数据。
self-dict是效率最差的存储方式,当数据很多并且不连续时,V8会放弃Map机制,改用self-dict方式存储。
图4给出了JS对象的内存部局。

申请JS对象时,对象的首地址指向Map(Map大小为:80byte),存在多个JS对象共用同一Map的情况,对象中还包括了Property back store和Element back store指针,是否包含其它成员依据情况而定,稍后见代码解释。
JS对象的成员方法存在哪里?文章开头处提到:它是普通的Property成员。通过下面的测试代码解释成员方法的存储方式。

1.  function person(name) {
2.      this.name=name;
3.      this.sayname=function(){console.log(this.name);}
4.  }
5.  worker = new person("Nicholas");
6.  worker.sayname();
7.  //分隔线..............................................................
8.  //分隔线..............................................................
9.  Bytecode Age: 0
10.           000001DAA2FA1E96 @    0 : 13 00             LdaConstant [0]
11.           000001DAA2FA1E98 @    2 : c2                Star1
12.           000001DAA2FA1E99 @    3 : 19 fe f8          Mov <closure>, r2
13.      0 E> 000001DAA2FA1E9C @    6 : 64 51 01 f9 02    CallRuntime [DeclareGlobals], r1-r2
14.    100 S> 000001DAA2FA1EA1 @   11 : 21 01 00          LdaGlobal [1], [0]
15.           000001DAA2FA1EA4 @   14 : c2                Star1
16.           000001DAA2FA1EA5 @   15 : 13 02             LdaConstant [2]
17.           000001DAA2FA1EA7 @   17 : c1                Star2
18.           000001DAA2FA1EA8 @   18 : 0b f9             Ldar r1
19.    109 E> 000001DAA2FA1EAA @   20 : 68 f9 f8 01 02    Construct r1, r2-r2, [2]
20.    107 E> 000001DAA2FA1EAF @   25 : 23 03 04          StaGlobal [3], [4]
21.    134 S> 000001DAA2FA1EB2 @   28 : 21 03 06          LdaGlobal [3], [6]
22.           000001DAA2FA1EB5 @   31 : c1                Star2
23.    141 E> 000001DAA2FA1EB6 @   32 : 2d f8 04 08       LdaNamedProperty r2, [4], [8]
24.           000001DAA2FA1EBA @   36 : c2                Star1
25.    141 E> 000001DAA2FA1EBB @   37 : 5c f9 f8 0a       CallProperty0 r1, r2, [10]
26.           000001DAA2FA1EBF @   41 : c3                Star0
27.    151 S> 000001DAA2FA1EC0 @   42 : a8                Return
28.  Constant pool (size = 5)
29.  000001DAA2FA1E29: [FixedArray] in OldSpace
30.   - map: 0x024008ac12c1 <Map>
31.   - length: 5
32.             0: 0x01daa2fa1d11 <FixedArray[2]>
33.             1: 0x01daa2fa1c09 <String[6]: #person>
34.             2: 0x01daa2fa1c39 <String[8]: #Nicholas>
35.             3: 0x01daa2fa1c21 <String[6]: #worker>
36.             4: 0x01daa2fa1c51 <String[7]: #sayname>

上半部分是js源码,下半部分是Bytecode。代码19行Construct构建JS对象person,并传递参数Nicholas,代码16,17行从常量池中取出Nicholas并存储到r2寄存器。而sayname成员是一个方法,因为lazy编译的原因,此时不做编译,而是在代码6行执行时才做编译,如下给出sayname成员编译后的字节码:

Bytecode Age: 0
   29 S> 000001DAA2FA21AE @    0 : 0b 03             Ldar a0
   38 E> 000001DAA2FA21B0 @    2 : 32 02 00 00       StaNamedProperty <this>, [0], [0]
   47 S> 000001DAA2FA21B4 @    6 : 7f 01 00 01       CreateClosure [1], [0], #1
   59 E> 000001DAA2FA21B8 @   10 : 32 02 02 02       StaNamedProperty <this>, [2], [2]
         000001DAA2FA21BC @   14 : 0e                LdaUndefined
   97 S> 000001DAA2FA21BD @   15 : a8                Return
Constant pool (size = 3)
000001DAA2FA2151: [FixedArray] in OldSpace
 - map: 0x024008ac12c1 <Map>
 - length: 3
           0: 0x024008ac5379 <String[4]: #name>
           1: 0x01daa2fa20f9 <SharedFunctionInfo sayname>
           2: 0x01daa2fa1c51 <String[7]: #sayname>

在上面的字节码中,看不到console.log,因为在此阶段V8仅执行sayname,虽然我们知道sayname的主体功能只有console.log,但V8还没有执行它,所以不编译。不执行时不编译,这就是lazy思想。
上述代码可以看出:sayname虽然是一个成员方法,但在JS对象内部,它只是普通的Property成员。

 

3 JS对象的创建过程

使用上面的测试用例,下面是创建person对象的源码位置:

1.  RUNTIME_FUNCTION(Runtime_NewObject) {
2.    HandleScope scope(isolate);
3.    DCHECK_EQ(2, args.length());
4.    CONVERT_ARG_HANDLE_CHECKED(JSFunction, target, 0);
5.    CONVERT_ARG_HANDLE_CHECKED(JSReceiver, new_target, 1);
6.    RETURN_RESULT_OR_FAILURE(
7.        isolate,
8.        JSObject::New(target, new_target, Handle<AllocationSite>::null()));
9.  }
10.  //分隔线.....................................
11.  MaybeHandle<JSObject> JSObject::New(Handle<JSFunction> constructor,
12.                                      Handle<JSReceiver> new_target,
13.                                      Handle<AllocationSite> site) {
14.    Isolate* const isolate = constructor->GetIsolate();
15.    DCHECK(constructor->IsConstructor());
16.    DCHECK(new_target->IsConstructor());
17.    DCHECK(!constructor->has_initial_map() ||
18.           !InstanceTypeChecker::IsJSFunction(
19.               constructor->initial_map().instance_type()));
20.    Handle<Map> initial_map;
21.    ASSIGN_RETURN_ON_EXCEPTION(
22.        isolate, initial_map,
23.        JSFunction::GetDerivedMap(isolate, constructor, new_target), JSObject);
24.    int initial_capacity = V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL
25.                               ? SwissNameDictionary::kInitialCapacity
26.                               : NameDictionary::kInitialCapacity;
27.    Handle<JSObject> result = isolate->factory()->NewFastOrSlowJSObjectFromMap(
28.        initial_map, initial_capacity, AllocationType::kYoung, site);
29.    isolate->counters()->constructed_objects()->Increment();
30.    isolate->counters()->constructed_objects_runtime()->Increment();
31.    return result;
32.  }

创建过程由RUNTIME_FUNCTION(Runtime_NewObject)开始,它是一个宏模板,在上篇文章中讲过,本文不在赘述。JSObject::New()方法新建JS对象,进入JSFunction::GetDerivedMap(isolate, constructor, new_target), JSObject);方法,源码如下:

1.  MaybeHandle<Map> JSFunction::GetDerivedMap(Isolate* isolate,
2.                                             Handle<JSFunction> constructor,
3.                                             Handle<JSReceiver> new_target) {
4.    EnsureHasInitialMap(constructor);
5.    Handle<Map> constructor_initial_map(constructor->initial_map(), isolate);
6.    if (*new_target == *constructor) return constructor_initial_map;
7.    Handle<Map> result_map;
8.    if (new_target->IsJSFunction()) {
9.      Handle<JSFunction> function = Handle<JSFunction>::cast(new_target);
10.       if (FastInitializeDerivedMap(isolate, function, constructor,
11.                                    constructor_initial_map)) {
12.         return handle(function->initial_map(), isolate);
13.       }
14.     }
15.     Handle<Object> prototype;
16.     if (new_target->IsJSFunction()) {
17.       Handle<JSFunction> function = Handle<JSFunction>::cast(new_target);
18.       if (function->has_prototype_slot()) {
19.         // Make sure the new.target.prototype is cached.
20.         EnsureHasInitialMap(function);
21.         prototype = handle(function->prototype(), isolate);
22.       } else {
23.         // No prototype property, use the intrinsict default proto further down.
24.         prototype = isolate->factory()->undefined_value();
25.       }
26.     } else {
27.  //省略很多..............
28.     }
29.     if (!prototype->IsJSReceiver()) {
30.  //省略很多..............
31.     }
32.     Handle<Map> map = Map::CopyInitialMap(isolate, constructor_initial_map);
33.     map->set_new_target_is_base(false);
34.     CHECK(prototype->IsJSReceiver());
35.     if (map->prototype() != *prototype)
36.       Map::SetPrototype(isolate, map, Handle<HeapObject>::cast(prototype));
37.     map->SetConstructor(*constructor);
38.     return map;
39.   }

Handle<JSFunction> constructor是构造函数person(测试代码),代码16行时构造函数的Map基本完成,这里设置并安装prototype。代码4行,EnsureHasInitialMap(constructor);很重要,它的作用是计算构造函数constructor的形状并生成Map。然后使用这个Map申请内存、创建对象的实例worker(测试样例代码),计算过程需要对它进行编译(如果之前没有编译过),代码如下:

1.  void JSFunction::EnsureHasInitialMap(Handle<JSFunction> function) {
2.    DCHECK(function->has_prototype_slot());
3.    DCHECK(function->IsConstructor() ||
4.           IsResumableFunction(function->shared().kind()));
5.    if (function->has_initial_map()) return;
6.    Isolate* isolate = function->GetIsolate();
7.    int expected_nof_properties =
8.        CalculateExpectedNofProperties(isolate, function);
9.    if (function->has_initial_map()) return;
10.    InstanceType instance_type;
11.    if (IsResumableFunction(function->shared().kind())) {
12.      instance_type = IsAsyncGeneratorFunction(function->shared().kind())
13.                          ? JS_ASYNC_GENERATOR_OBJECT_TYPE
14.                          : JS_GENERATOR_OBJECT_TYPE;
15.    } else {
16.      instance_type = JS_OBJECT_TYPE;
17.    }
18.    int instance_size;
19.    int inobject_properties;
20.    CalculateInstanceSizeHelper(instance_type, false, 0, expected_nof_properties,
21.                                &instance_size, &inobject_properties);
22.    Handle<Map> map = isolate->factory()->NewMap(instance_type, instance_size,
23.                                                 TERMINAL_FAST_ELEMENTS_KIND,
24.                                                 inobject_properties);
25.    Handle<HeapObject> prototype;
26.    if (function->has_instance_prototype()) {
27.      prototype = handle(function->instance_prototype(), isolate);
28.    } else {
29.      prototype = isolate->factory()->NewFunctionPrototype(function);
30.    }
31.    DCHECK(map->has_fast_object_elements());
32.    DCHECK(prototype->IsJSReceiver());
33.    JSFunction::SetInitialMap(isolate, function, map, prototype);
34.    map->StartInobjectSlackTracking();
35.  }

代码5行,如果已经有Map,不用再计算了,返回。代码7行,计算构造函数的属性值,这里进行编译。代26~30行,生成prototype(注意区分:这里是生成,然后才是前面提到的设置和安全),构造函数是第一次生成,没有prototype,进入代码29行。注意: 这里可以看出,同一个构函数的不同实例之间是共用一个prototype,因为prototype设置在构造函数person上,我们用person实例多个对象时,只有在person初次生成时才执行代码29行。
再来看代码7行,源码如下:

1.  int JSFunction::CalculateExpectedNofProperties(Isolate* isolate,
2.                                                 Handle<JSFunction> function) {
3.    int expected_nof_properties = 0;
4.    for (PrototypeIterator iter(isolate, function, kStartAtReceiver);
5.         !iter.IsAtEnd(); iter.Advance()) {
6.      Handle<JSReceiver> current =
7.          PrototypeIterator::GetCurrent<JSReceiver>(iter);
8.      if (!current->IsJSFunction()) break;
9.      Handle<JSFunction> func = Handle<JSFunction>::cast(current);
10.      Handle<SharedFunctionInfo> shared(func->shared(), isolate);
11.      IsCompiledScope is_compiled_scope(shared->is_compiled_scope(isolate));
12.      if (is_compiled_scope.is_compiled() ||
13.          Compiler::Compile(isolate, func, Compiler::CLEAR_EXCEPTION,
14.                            &is_compiled_scope)) {
15.        DCHECK(shared->is_compiled());
16.        int count = shared->expected_nof_properties();
17.        if (expected_nof_properties <= JSObject::kMaxInObjectProperties - count) {
18.          expected_nof_properties += count;
19.        } else {
20.          return JSObject::kMaxInObjectProperties;
21.        }
22.      } else {
23.        continue;
24.      }
25.    }
26.    if (expected_nof_properties > 0) {
27.      expected_nof_properties += 8;
28.      if (expected_nof_properties > JSObject::kMaxInObjectProperties) {
29.        expected_nof_properties = JSObject::kMaxInObjectProperties;
30.      }
31.    }
32.    return expected_nof_properties;
33.  }

Handle<JSFunction> function是构造函数person,代码13行进行编译并计算对象的属性值数量。代码28行,会与MaxInObject比较,如果大于MaxInObject,那么属性值数量就是MaxInObject。前面提到JS对象中In-Object数量有限,MaxInObject正是它的最大数量。多出的属性值在后面会作处理——放入图1的属性值存储区。
回到JSObject::New()方法中代码27行,用Map去申请JS对象的内存,后面就是对象实例化的参数设置等等,请读者根据图5的函数堆栈自行跟踪。

好了,今天到这里,下次见。

恳请读者批评指正、提出宝贵意见
微信:qq9123013 备注:v8交流 邮箱:v8blink@outlook.com

本文由灰豆原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/257484

安全客 - 有思想的安全新媒体

分享到:微信
+11赞
收藏
灰豆
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66