DEV Community

Cover image for Let’s Understand Chrome V8: How Does V8 Implement a JavaScript Object?
灰豆
灰豆

Posted on • Originally published at javascript.plainenglish.io

Let’s Understand Chrome V8: How Does V8 Implement a JavaScript Object?

Chapter 17: JavaScript object memory layout and new method.
Original source: https://javascript.plainenglish.io/lets-understand-chrome-v8-how-does-v8-implement-a-javascript-object-ddde32120bb5
Welcome to other chapters of Let’s Understand Chrome V8


We know that the JavaScript object is a set of properties and elements. For performance and memory reasons, V8 designed several different representations of properties such as in-object, slow property, and self-dict. In this chapter, let’s examine the JavaScript object in a little more depth.

1. JavaScript object layout

The following Figure 1 shows what a basic JavaScript object looks like in memory.

Image description
Elements and properties are stored in two separate data structures which makes adding and accessing properties or elements more efficient for different usage patterns.

Elements are mainly used for the various Array.prototype methods such as pop or slice. Given that these functions access properties in consecutive ranges, V8 also represents them as simple arrays internally — most of the time.

Named properties are stored in a similar way in a separate array. However, unlike elements, we cannot simply use the key to deduce their position within the properties array; we need some additional metadata. In V8 every JavaScript object has a HiddenClass associated. The HiddenClass stores information about the shape of an object, and among other things, a mapping from property names to indices into the properties, more details are in chapter 14.

Image description
In Figure 2, there are elements, properties, and in-object properties. Unlike elements or properties, the in-object properties are stored in the Object itself, which means you can access them directly without map. The amount of the in-object is allocated when creating the JavaScript object. (The amount may be dissimilar in different V8 versions)

Image description
In Figure 3, there are three different named property types: in-object, fast and slow/dictionary.

(1) In-object properties are stored directly on the object itself and provide the fastest access.

(2) Fast properties live in the properties store, all the meta information is stored in the descriptor array on the HiddenClass.

(3) Slow properties live in a self-contained properties dictionary, meta information is no longer shared through the HiddenClass.

Image description
Figure 4 shows the JavaScript object memory layout.

All V8 managed heap objects must have a map pointer that is stored at the first address. Of course, also have elements and properties. The descriptor is an important member of the map, which is responsible for describing the properties of an object, which we will talk about in the future.

In the following case, the sayname is a function that is equal to console.log.

Let’s think about another question: where is the function of an object stored? namely, where is the sayname?

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.  //separation........................................
8.  //separation........................................
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>
Enter fullscreen mode Exit fullscreen mode

The first part is JavaScript code, the last is corresponding bytecodes.

Lines 19–21 use the person object to construct an instance named worker.

Line 23 load the property that name is sayname.

Do you understand line23? It tells us that the sayname is just a property, regardless of its type. If you go a little more in-depth, you will see that the console and log are properties also.

So, in a JavaScript Object, all functions are treated as properties.

2. New JavaScript object

To debug the above case, we will step into the following code:

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.  //separation.....................................
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.  }
Enter fullscreen mode Exit fullscreen mode

The RUNTIME_FUNCTION(Runtime_NewObject) is a MACRO that we mentioned in the last chapter. It calls the JSObject::New() to create a new object. The JSObject::New() calls JSFunction::GetDerivedMap() to allocate a new map.

In JSFunction::GetDerivedMap(), the following function will be called.

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.  }
Enter fullscreen mode Exit fullscreen mode

Line 7, returns the number of properties expected by the constructor.

Lines 26 to 30 generate the prototype, the keyword function is a constructor. In our case, the function is person, the prototype is null since the person is the first-time execution, so line 29 is executed.

The generated prototype is bound to the constructor and is shared by all instances, that is the prototype principle that V8 implements.

Below is the CalculateExpectedNofProperties which is called in line 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.  }
Enter fullscreen mode Exit fullscreen mode

Line 28, the MaxInObject is the max amount of in-object, and the exceeded properties are stored in the properties list. Figure 5 shows the call stack.

Image description

References

Fast properties in V8

Okay, that wraps it up for this share. I’ll see you guys next time, take care!

Please reach out to me if you have any issues. WeChat: qq9123013 Email: v8blink@outlook.com

Top comments (0)