Home > Article > Web Front-end > Detailed explanation of JavaScript V8 Object memory structure and attribute access
In the 1990s, with the release of the Netscape browser, JavaScript first came into people’s sight. Later, with the advent of the large-scale application of AJAX and the era of rich client and single-page applications, JavaScript occupied an increasingly important position in Web development. In early JavaScript engines, performance increasingly became a bottleneck in developing web applications. The design goal of the V8 engine is to ensure the execution efficiency of large-scale JavaScript applications. In many tests, it can be clearly found that the performance of V8 is better than JScript (Internet Explorer), SpiderMonkey (Firefox), and JavaScriptCore (Safari). According to the official documentation of V8 Introduction, it mainly focuses on performance optimization from three aspects: attribute access, dynamic machine code generation and efficient garbage collection. Object is undoubtedly one of the most important data types in JavaScript. In this article, we explain its internal structure in detail. The inheritance diagram is as follows:
The newly allocated JavaScript object structure in V8 is as follows:
[ class / map ] -> ... ; 指向内部类 [ properties ] -> [empty array] [ elements ] -> [empty array] ; 数值类型名称的属性 [ reserved #1 ] -\ [ reserved #2 ] | [ reserved #3 ] }- in object properties,即预分配的内存空间 ............... | [ reserved #N ] -/
When creating a new object, V8 will create a pre-allocated memory area to store so-called in-object properties. The size of the pre-allocated area is determined by the number of parameters in the constructor (this.field = expr
). When you plan to add a new attribute to an object, V8 will first try to put it into the so-called in-order slot. When the in-object slot is overloaded, V8 will try to add the new attribute to the out-of- object property list. The mapping relationship between attribute names and attribute subscripts is stored in the so-called hidden class, for example { a: 1, b: 2, c: 3, d: 4}
The storage method of the object may be as follows:
[ class ] -> [a: in obj #1, b: in obj #2, c: out obj #1, d: out obj #2] [ properties ] -> [ 3 ][ 4 ] ; this is linear array [ elements ] [ 1 ] [ 2 ]
As the number of attributes increases, V8 will switch back to the traditional dictionary mode/hash table mode:
[ class ] -> [ OBJECT IS IN DICTIONARY MODE ] [ properties ] -> [a: 1, b: 2, c: 3, d: 4, e: 5] ; this is classical hash table [ elements ]
V8 Design Elements
A tour of V8: object representation
Demystifying v8 and JavaScript Performance
V8 Docs :Object Class Reference
How does V8 manage the memory of object instances?
As Dynamic language, JavaScript allows us to define objects in a very flexible way, for example:
obj.prop obj["prop"]
Referring to the description in the JavaScript definition specification, the attribute name is always a string, even if you use a non-string name , will also be converted to string type implicitly. For example, if you create an array and access it with a numerical subscript, V8 still converts it into a string and then indexes it, so the following method will achieve the same effect:
obj[1]; // obj["1"]; // names for the same property obj[1.0]; // var o = { toString: function () { return "-1.5"; } }; obj[-1.5]; // also equivalent obj[o]; // since o is converted to string
And Array in JavaScript It is just an object that contains additional length
attributes. length
will return the result of the current maximum subscript plus one (at this time, the string subscript will be converted into a numerical type for calculation):
var a = new Array(); a[100] = "foo"; a.length; //101 a[undefined] = 'a'; a.length; //0
Function
is essentially an object, but the length
property will return the length of the parameter:
> a = ()=>{} [Function: a] > a.length 0 > a = (b)=>{} [Function: a] > a.length 1
As a dynamically typed language, object properties in JavaScript can be dynamically added and deleted at runtime, which means that the structure of the entire object will change frequently. Most JavaScript engines tend to use dictionary-type data structures to store object properties. Every time a property is accessed, the engine needs to dynamically locate the subscript address corresponding to the property in the inner layer and then read the value. This method is easier to implement, but will lead to poor performance. In other static languages like Java and Smalltalk, member variables have their fixed offset addresses in memory determined during the compilation phase, and only a single instruction is needed to load them from memory when accessing attributes. V8 uses the method of dynamically creating hidden inner classes to dynamically record the memory address of the attribute in the object, thereby improving the overall attribute access speed. To summarize, every time a new property is added to an object, V8 automatically corrects its hidden inner class. Let's first experience the existence of hidden classes through an experiment:
var PROPERTIES = 10000000; var o = {}; var start = +new Date; for (var i = 0; i < PROPERTIES; i++) { o[i] = i; } console.log(+new Date - start); function O(size) { for (var i = 0; i < size; i++) { this[i] = null; } } var o = new O(PROPERTIES); var start = +new Date; for (var i = 0; i < PROPERTIES; i++) { o[i] = i; } console.log(+new Date - start); class OClass { constructor(size){ for (var i = 0; i < size; i++) { this[i] = null; } } } var o = new OClass(PROPERTIES); var start = +new Date; for (var i = 0; i < PROPERTIES; i++) { o[i] = i; } console.log(+new Date - start);
The execution results of this program are as follows:
// Babel 下结果 385 37 49 // Chrome 下结果 416 32 31
In the first implementation, each time it is an objecto
When setting a new attribute, V8 will create a new hidden internal class (internally called Map) to store the new memory address to optimize the attribute lookup speed. In the second implementation, we initialize the inner class when creating a new object, so that V8 can locate these properties with high performance when assigning properties. The third implementation uses ES6 Class, which has the best performance under pure V8. Next, we will elaborate on how the hidden class works. Suppose we define a function that describes the point:
function Point(x, y) { this.x = x; this.y = y; }
When we execute the new Point(x,y)
statement, V8 will create Some new Point
object. During the creation process, V8 will first create a hidden internal class called C0
. Because no attributes have been added to the object, the hidden class is still empty at this time:
接下来调用首个赋值语句this.x = x;
为当前Point
对象创建了新的属性x
,此时 V8 会基于C0
创建另一个隐藏类C1
来替换C0
,然后在C1
中存放对象属性x
的内存位置信息:
这里从C0
到C1
的变化称为转换(Transitions),当我们为同一个类型的对象添加新的属性时,并不是每次都会创建新的隐藏类,而是多个对象会共用某个符合转换条件的隐藏类。接下来继续执行this.y = y
这一条语句,会为Point
对象创建新的属性。此时 V8 会进行以下步骤:
基于C1
创建另一个隐藏类C1
,并且将关于属性y
的位置信息写入到C2
中。
更新C1
为其添加转换信息,即当为Point
对象添加属性 y
时,应该转换到隐藏类 C2
。
整个过程的伪代码描述如下:
<Point object is allocated> Class C0 "x": TRANSITION to C1 at offset 0 this.x = x; Class C1 "x": FIELD at offset 0 "y": TRANSITION to C2 at offset 1 this.y = y; Map C2 "x": FIELD at offset 0 "y": FIELD at offset 1
我们在上文中提及,如果每次添加新的属性时都创建新的隐藏类无疑是极大的性能浪费,实际上当我们再次创建新的Point
对象时,V8 并不会创建新的隐藏类而是使用已有的,过程描述如下:
初始化新的Point
对象,并将隐藏类指向C0
。
添加x
属性时,遵循隐藏类的转换原则指向到C1
, 并且根据C1
指定的偏移地址写入x
。
添加y
属性时,遵循隐藏类的转换原则指向到C2
,并且根据C2
指定的偏移地址写入y
。
另外我们在上文以链表的方式描述转换,实际上真实场景中 V8 会以树的结构来描述转换及其之间的关系,这样就能够用于类似于下面的属性一致而赋值顺序颠倒的场景:
function Point(x, y, reverse) { if (reverse) { this.x = x; this.y = y; } else { this.y = x; this.x = y; } }
JavaScript 中并没有类的概念(语法糖除外),因此对于方法的调用处理会难于 C++ 或者 Java。下面这个例子中,distance
方法可以被看做Point
的普通属性之一,不过其并非原始类型的数据,而是指向了另一个函数:
function Point(x, y) { this.x = x; this.y = y; this.distance = PointDistance; } function PointDistance(p) { var dx = this.x - p.x; var dy = this.y - p.y; return Math.sqrt(dx*dx + dy*dy); }
如果我们像上文介绍的普通的 in-object 域一样来处理distance
属性,那么无疑会带来较大的内存浪费,毕竟每个对象都要存放一段外部函数引用(Reference 的内存占用往往大于原始类型)。C++ 中则是以指向多个虚函数的虚函数表(V-Tables)解决这个问题。每个包含虚函数的类的实例都会指向这个虚函数表,当调用某个虚函数时,程序会自动从虚函数表中加载该函数的地址信息然后转向到该地址调用。V8 中我们已经使用了隐藏类这一共享数据结构,因此可以很方便地改造下就可以。我们引入了所谓 Constant Functions 的概念,某个 Constant Function 即代表了对象中仅包含某个名字,而具体的属性值存放在描述符本身的概念:
<Point object is allocated> Class C0 "x": TRANSITION to C1 at offset 0 this.x = x; Class C1 "x": FIELD at offset 0 "y": TRANSITION to C2 at offset 1 this.y = y; Class C2 "x": FIELD at offset 0 "y": FIELD at offset 1 "distance": TRANSITION to C3 <PointDistance> this.distance = PointDistance; Class C3 "x": FIELD at offset 0 "y": FIELD at offset 1 "distance": CONSTANT_FUNCTION <PointDistance>
注意,在这里如果我们将PointDistance
重定义指向了其他函数,那么这个转换也会自动失效,V8 会创建新的隐藏类。另一种解决这个问题的方法就是使用原型,每个构造函数都会有所谓的Prototype
属性,该属性会自动成为对象的原型链上的一环,上面的例子可以改写为以下方式:
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.distance = function(p) { var dx = this.x - p.x; var dy = this.y - p.y; return Math.sqrt(dx*dx + dy*dy); } ... var u = new Point(1, 2); var v = new Point(3, 4); var d = u.distance(v);
V8 同样会把原型链上的方法在隐藏类中映射为 Constant Function 描述符,而调用原型方法往往会比调用自身方法慢一点,毕竟引擎不仅要去扫描自身的隐藏类,还要去扫描原型链上对象的隐藏类才能得知真正的函数调用地址。不过这个不会对于代码的性能造成明显的影响,因此写代码的时候也不必小心翼翼的避免这个。
对于复杂属性的对象,V8 会使用所谓的字典模式(Dictionary Mode)来存储对象,也就是使用哈希表来存放键值信息,这种方式存储开销会小于上文提到的包含了隐藏类的方式,不过查询速度会远小于前者。初始状态下,哈希表中的所有的键与值都被设置为了undefined
,当插入新的数据时,计算得出的键名的哈希值的低位会被当做初始的存储索引地址。如果此地址已经被占用了,V8 会尝试向下一个地址进行插入,直到插入成功,伪代码表述如下:
// 插入 insert(table, key, value): table = ensureCapacity(table, length(table) + 1) code = hash(key) n = capacity(table) index = code (mod n) while getKey(table, index) is not undefined: index += 1 (mod n) set(table, index, key, value) //查找 lookup(table, key): code = hash(key) n = capacity(table) index = code (mod n) k = getKey(table, index) while k is not null or undefined and k != key: index += 1 (mod n) k = getKey(table, index) if k == key: return getValue(table, index) else: return undefined
尽管计算键名哈希值与比较的速度会比较快,但是每次读写属性的时候都进行这么多步骤无疑会大大拉低速度,因此 V8 尽可能地会避免使用这种存储方式。
V8 中将属性名为非负整数(0、1、2……)的属性称为Element,每个对象都有一个指向Element数组的指针,其存放和其他属性是分开的。注意,隐藏类中并不包含 Element 的描述符,但可能包含其它有着不同 Element 类型的同一种隐藏类的转换描述符。大多数情况下,对象都会有 Fast Element,也就是说这些 Element 以连续数组的形式存放。有三种不同的 Fast Element:
Fast small integers
Fast doubles
Fast values
根据标准,JavaScript 中的所有数字都理应以64位浮点数形式出现。因此 V8 尽可能以31位带符号整数来表达数字(最低位总是0,这有助于垃圾回收器区分数字和指针)。因此含有Fast small integers类型的对象,其 Element 类型只会包含这样的数字。如果需要存储小数、大整数或其他特殊值,如-0,则需要将数组提升为 Fast doubles。于是这引入了潜在的昂贵的复制-转换操作,但通常不会频繁发生。Fast doubles 仍然是很快的,因为所有的数字都是无封箱存储的。但如果我们要存储的是其他类型,比如字符串或者对象,则必须将其提升为普通的 Fast Element 数组。
JavaScript 不提供任何确定存储元素多少的办法。你可能会说像这样的办法,new Array(100)
,但实际上这仅仅针对Array
构造函数有用。如果你将值存在一个不存在的下标上,V8会重新开辟更大的内存,将原有元素复制到新内存。V8 可以处理带空洞的数组,也就是只有某些下标是存有元素,而期间的下标都是空的。其内部会安插特殊的哨兵值,因此试图访问未赋值的下标,会得到undefined
。当然,Fast Element 也有其限制。如果你在远远超过当前数组大小的下标赋值,V8 会将数组转换为字典模式,将值以哈希表的形式存储。这对于稀疏数组来说很有用,但性能上肯定打了折扣,无论是从转换这一过程来说,还是从之后的访问来说。如果你需要复制整个数组,不要逆向复制(索引从高到低),因为这几乎必然触发字典模式。
// 这会大大降低大数组的性能 function copy(a) { var b = new Array(); for (var i = a.length - 1; i >= 0; i--) b[i] = a[i]; return b; }
由于普通的属性和数字式属性分开存放,即使数组退化为字典模式,也不会影响到其他属性的访问速度(反之亦然)。
// http://www.php.cn/ class V8_EXPORT Object : public Value { public: V8_DEPRECATE_SOON("Use maybe version", bool Set(Local<Value> key, Local<Value> value)); V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context, Local<Value> key, Local<Value> value); V8_DEPRECATE_SOON("Use maybe version", bool Set(uint32_t index, Local<Value> value)); V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context, uint32_t index, Local<Value> value); // Implements CreateDataProperty (ECMA-262, 7.3.4). // // Defines a configurable, writable, enumerable property with the given value // on the object unless the property already exists and is not configurable // or the object is not extensible. // // Returns true on success. V8_WARN_UNUSED_RESULT Maybe<bool> CreateDataProperty(Local<Context> context, Local<Name> key, Local<Value> value); V8_WARN_UNUSED_RESULT Maybe<bool> CreateDataProperty(Local<Context> context, uint32_t index, Local<Value> value); // Implements DefineOwnProperty. // // In general, CreateDataProperty will be faster, however, does not allow // for specifying attributes. // // Returns true on success. V8_WARN_UNUSED_RESULT Maybe<bool> DefineOwnProperty( Local<Context> context, Local<Name> key, Local<Value> value, PropertyAttribute attributes = None); // Sets an own property on this object bypassing interceptors and // overriding accessors or read-only properties. // // Note that if the object has an interceptor the property will be set // locally, but since the interceptor takes precedence the local property // will only be returned if the interceptor doesn't return a value. // // Note also that this only works for named properties. V8_DEPRECATED("Use CreateDataProperty / DefineOwnProperty", bool ForceSet(Local<Value> key, Local<Value> value, PropertyAttribute attribs = None)); V8_DEPRECATE_SOON("Use CreateDataProperty / DefineOwnProperty", Maybe<bool> ForceSet(Local<Context> context, Local<Value> key, Local<Value> value, PropertyAttribute attribs = None)); V8_DEPRECATE_SOON("Use maybe version", Local<Value> Get(Local<Value> key)); V8_WARN_UNUSED_RESULT MaybeLocal<Value> Get(Local<Context> context, Local<Value> key); V8_DEPRECATE_SOON("Use maybe version", Local<Value> Get(uint32_t index)); V8_WARN_UNUSED_RESULT MaybeLocal<Value> Get(Local<Context> context, uint32_t index); V8_DEPRECATED("Use maybe version", PropertyAttribute GetPropertyAttributes(Local<Value> key)); V8_WARN_UNUSED_RESULT Maybe<PropertyAttribute> GetPropertyAttributes( Local<Context> context, Local<Value> key); V8_DEPRECATED("Use maybe version", Local<Value> GetOwnPropertyDescriptor(Local<String> key)); V8_WARN_UNUSED_RESULT MaybeLocal<Value> GetOwnPropertyDescriptor( Local<Context> context, Local<String> key); V8_DEPRECATE_SOON("Use maybe version", bool Has(Local<Value> key)); V8_WARN_UNUSED_RESULT Maybe<bool> Has(Local<Context> context, Local<Value> key); V8_DEPRECATE_SOON("Use maybe version", bool Delete(Local<Value> key)); // TODO(dcarney): mark V8_WARN_UNUSED_RESULT Maybe<bool> Delete(Local<Context> context, Local<Value> key); V8_DEPRECATED("Use maybe version", bool Has(uint32_t index)); V8_WARN_UNUSED_RESULT Maybe<bool> Has(Local<Context> context, uint32_t index); V8_DEPRECATED("Use maybe version", bool Delete(uint32_t index)); // TODO(dcarney): mark V8_WARN_UNUSED_RESULT Maybe<bool> Delete(Local<Context> context, uint32_t index); V8_DEPRECATED("Use maybe version", bool SetAccessor(Local<String> name, AccessorGetterCallback getter, AccessorSetterCallback setter = 0, Local<Value> data = Local<Value>(), AccessControl settings = DEFAULT, PropertyAttribute attribute = None)); V8_DEPRECATED("Use maybe version", bool SetAccessor(Local<Name> name, AccessorNameGetterCallback getter, AccessorNameSetterCallback setter = 0, Local<Value> data = Local<Value>(), AccessControl settings = DEFAULT, PropertyAttribute attribute = None)); // TODO(dcarney): mark V8_WARN_UNUSED_RESULT Maybe<bool> SetAccessor(Local<Context> context, Local<Name> name, AccessorNameGetterCallback getter, AccessorNameSetterCallback setter = 0, MaybeLocal<Value> data = MaybeLocal<Value>(), AccessControl settings = DEFAULT, PropertyAttribute attribute = None); void SetAccessorProperty(Local<Name> name, Local<Function> getter, Local<Function> setter = Local<Function>(), PropertyAttribute attribute = None, AccessControl settings = DEFAULT); Maybe<bool> HasPrivate(Local<Context> context, Local<Private> key); Maybe<bool> SetPrivate(Local<Context> context, Local<Private> key, Local<Value> value); Maybe<bool> DeletePrivate(Local<Context> context, Local<Private> key); MaybeLocal<Value> GetPrivate(Local<Context> context, Local<Private> key); V8_DEPRECATE_SOON("Use maybe version", Local<Array> GetPropertyNames()); V8_WARN_UNUSED_RESULT MaybeLocal<Array> GetPropertyNames( Local<Context> context); V8_WARN_UNUSED_RESULT MaybeLocal<Array> GetPropertyNames( Local<Context> context, KeyCollectionMode mode, PropertyFilter property_filter, IndexFilter index_filter); V8_DEPRECATE_SOON("Use maybe version", Local<Array> GetOwnPropertyNames()); V8_WARN_UNUSED_RESULT MaybeLocal<Array> GetOwnPropertyNames( Local<Context> context); V8_WARN_UNUSED_RESULT MaybeLocal<Array> GetOwnPropertyNames( Local<Context> context, PropertyFilter filter); Local<Value> GetPrototype(); V8_DEPRECATED("Use maybe version", bool SetPrototype(Local<Value> prototype)); V8_WARN_UNUSED_RESULT Maybe<bool> SetPrototype(Local<Context> context, Local<Value> prototype); Local<Object> FindInstanceInPrototypeChain(Local<FunctionTemplate> tmpl); V8_DEPRECATED("Use maybe version", Local<String> ObjectProtoToString()); V8_WARN_UNUSED_RESULT MaybeLocal<String> ObjectProtoToString( Local<Context> context); Local<String> GetConstructorName(); Maybe<bool> SetIntegrityLevel(Local<Context> context, IntegrityLevel level); int InternalFieldCount(); V8_INLINE static int InternalFieldCount( const PersistentBase<Object>& object) { return object.val_->InternalFieldCount(); } V8_INLINE Local<Value> GetInternalField(int index); void SetInternalField(int index, Local<Value> value); V8_INLINE void* GetAlignedPointerFromInternalField(int index); V8_INLINE static void* GetAlignedPointerFromInternalField( const PersistentBase<Object>& object, int index) { return object.val_->GetAlignedPointerFromInternalField(index); } void SetAlignedPointerInInternalField(int index, void* value); void SetAlignedPointerInInternalFields(int argc, int indices[], void* values[]); // Testers for local properties. V8_DEPRECATED("Use maybe version", bool HasOwnProperty(Local<String> key)); V8_WARN_UNUSED_RESULT Maybe<bool> HasOwnProperty(Local<Context> context, Local<Name> key); V8_WARN_UNUSED_RESULT Maybe<bool> HasOwnProperty(Local<Context> context, uint32_t index); V8_DEPRECATE_SOON("Use maybe version", bool HasRealNamedProperty(Local<String> key)); V8_WARN_UNUSED_RESULT Maybe<bool> HasRealNamedProperty(Local<Context> context, Local<Name> key); V8_DEPRECATE_SOON("Use maybe version", bool HasRealIndexedProperty(uint32_t index)); V8_WARN_UNUSED_RESULT Maybe<bool> HasRealIndexedProperty( Local<Context> context, uint32_t index); V8_DEPRECATE_SOON("Use maybe version", bool HasRealNamedCallbackProperty(Local<String> key)); V8_WARN_UNUSED_RESULT Maybe<bool> HasRealNamedCallbackProperty( Local<Context> context, Local<Name> key); V8_DEPRECATED( "Use maybe version", Local<Value> GetRealNamedPropertyInPrototypeChain(Local<String> key)); V8_WARN_UNUSED_RESULT MaybeLocal<Value> GetRealNamedPropertyInPrototypeChain( Local<Context> context, Local<Name> key); V8_DEPRECATED( "Use maybe version", Maybe<PropertyAttribute> GetRealNamedPropertyAttributesInPrototypeChain( Local<String> key)); V8_WARN_UNUSED_RESULT Maybe<PropertyAttribute> GetRealNamedPropertyAttributesInPrototypeChain(Local<Context> context, Local<Name> key); V8_DEPRECATED("Use maybe version", Local<Value> GetRealNamedProperty(Local<String> key)); V8_WARN_UNUSED_RESULT MaybeLocal<Value> GetRealNamedProperty( Local<Context> context, Local<Name> key); V8_DEPRECATED("Use maybe version", Maybe<PropertyAttribute> GetRealNamedPropertyAttributes( Local<String> key)); V8_WARN_UNUSED_RESULT Maybe<PropertyAttribute> GetRealNamedPropertyAttributes( Local<Context> context, Local<Name> key); bool HasNamedLookupInterceptor(); bool HasIndexedLookupInterceptor(); int GetIdentityHash(); // TODO(dcarney): take an isolate and optionally bail out? Local<Object> Clone(); Local<Context> CreationContext(); bool IsCallable(); bool IsConstructor(); V8_DEPRECATED("Use maybe version", Local<Value> CallAsFunction(Local<Value> recv, int argc, Local<Value> argv[])); V8_WARN_UNUSED_RESULT MaybeLocal<Value> CallAsFunction(Local<Context> context, Local<Value> recv, int argc, Local<Value> argv[]); V8_DEPRECATED("Use maybe version", Local<Value> CallAsConstructor(int argc, Local<Value> argv[])); V8_WARN_UNUSED_RESULT MaybeLocal<Value> CallAsConstructor( Local<Context> context, int argc, Local<Value> argv[]); V8_DEPRECATE_SOON("Keep track of isolate correctly", Isolate* GetIsolate()); static Local<Object> New(Isolate* isolate); V8_INLINE static Object* Cast(Value* obj); private: Object(); static void CheckCast(Value* obj); Local<Value> SlowGetInternalField(int index); void* SlowGetAlignedPointerFromInternalField(int index); };
以上就是JavaScript V8 Object 内存结构与属性访问详解的内容,更多相关内容请关注PHP中文网(www.php.cn)!