图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?

简介: 图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?

说明

图解 Google V8 学习笔记



为什么静态语言的效率更高?


静态语言中,可以直接通过偏移量查询来查询对象的属性值。

比如下面例子:

b82c4053931f41afbf65f03e8fd892d7.png


JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少。V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。


C++ 代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的。编译器会直接将 x 相对于 start 的地址写进汇编指令中,使用了对象 start 中的 x 属性时,CPU 直接去内存地址中取出该内容即可,没有任何中间的查找环节。



什么是隐藏类 (Hidden Class)?


V8 引入隐藏类的动机

因为 JavaScript 是一门动态语言,对象属性在执行过程中是可以被修改的,这就导致了在运行时,V8 无法知道对象的完整形状,那么当查找对象中的属性时,V8 就需要经过一系列复杂的步骤才能获取到对象属性。



V8 怎么优化 JavaScript 中的对象

V8 会为每个对象创建一个隐藏类,对象的隐藏类中记录了该对象一些基础的布局信息:


   对象中所包含的所有的属性;

   每个属性相对于对象的偏移量。


在 V8 中,把隐藏类又称为 map,每个对象都有一个 map 属性,其值指向内存中的隐藏类。有了隐藏类,V8 就可以根据隐藏类中描述的偏移地址获取对应的属性值,这样就省去了复杂的查找流程。



两个假设


隐藏类是建立在两个假设基础之上的:


   对象创建好了之后就不会添加新的属性;


   对象创建好了之后也不会删除属性。


一旦对象的形状发生了改变,这意味着 V8 需要为对象重建新的隐藏类,这就会带来效率问题。



隐藏类是怎么工作的?


先看下面代码


let point = {
  x:100,
  y:200
}




当 V8 执行到这段代码时,会先为 point 对象创建一个隐藏类。

隐藏类描述了对象的属性布局:


6e56abdc4b65425cbe4d0f0aded54cd1.png


point 对象和 map 之间的关系:


afe055cb98c046abad668232f8f47487.png



有了 map 之后,当你使用 point.x 访问 x 属性时,V8 会查询 point 的 map 中 x 属性相对 point 对象的偏移量,然后将 point 对象的起始位置加上偏移量,就得到了 x 属性的值在内存中的位置,有了这个位置也就拿到了 x 的值,这样就省去了一个比较复杂的查找过程。



实战1:通过 v8-debug 查看隐藏类


可以看看这篇文章:V8 编译浅谈,下图来自这篇文章的截图,里面就提到了一个命令:允许在源代码中使用 V8 提供的原生 API 语法 --allow-natives-syntax

cc4b3dbbe9d34f21ab3d51e73d5724c8.png


也可以自己去 v8-debug-hlep.txt 的文档里去找:


f367366118fa419d96fae975d5d1e9c6.png

下面在 kaimo.js 文件里添加下面代码:


ca9a8e16202e448eb7f81374bb3100a6.png



let kaimo = {
    x:100,
    y:200
};
%DebugPrint(kaimo);


2d7717d1d55848bc80d61ecf55662b0d.png


然后在控制台输入命令,可以打印出 kaimo 对象的基础结构:

v8-debug --allow-natives-syntax kaimo.js


kaimo 的内存结构如下:

DebugPrint: 00000032000CA015: [JS_OBJECT_TYPE]
 - map: 0x003200287a59 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x003200244215 <Object map = 00000032002821E9>
 - elements: 0x003200002261 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x003200002261 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000003200253589: [String] in OldSpace: #x: 100 (const data field 0), location: in-object
    0000003200253599: [String] in OldSpace: #y: 200 (const data field 1), location: in-object
 }
0000003200287A59: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x003200287a31 <Map[20](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0032001c4581 <Cell value= 1>
 - instance descriptors (own) #2: 0x0032000ca045 <DescriptorArray[2]>
 - prototype: 0x003200244215 <Object map = 00000032002821E9>
 - constructor: 0x003200243e29 <JSFunction Object (sfi = 00000032001DB6D9)>
 - dependent code: 0x0032000021e9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0



64770271068d42e9acc4e7c60fc67f8d.png

可以看到,kaimo 对象的第一个属性就是 map,它指向了 0x003200287a59 这个地址,这个地址就是 V8 为 kaimo 对象创建的隐藏类,除了 map 属性之外,还有 prototype 属性,elements 属性和 properties 属性。



多个对象共用一个隐藏类


什么是对象的形状相同?


要满足以下两点:


   相同的属性名称;


   相等的属性个数。


复用同一个隐藏类的好处


如果两个对象的形状是相同的,V8 就会为其复用同一个隐藏类,好处:


   减少隐藏类的创建次数,也间接加速了代码的执行速度;


   减少了隐藏类的存储空间。



实战2:多个对象共用一个隐藏类


替换 kaimo.js 的代码,

let kaimo666 = {
    x: 6,
    y: 66
};
let kaimo777 = {
    x: 7,
    y: 77
};
%DebugPrint(kaimo666);
%DebugPrint(kaimo777);


然后再执行下面命令:

v8-debug --allow-natives-syntax kaimo.js


输出结果如下:

DebugPrint: 00000171000CA05D: [JS_OBJECT_TYPE]
 - map: 0x017100287a59 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x017100244215 <Object map = 00000171002821E9>
 - elements: 0x017100002261 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x017100002261 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000017100253589: [String] in OldSpace: #x: 6 (const data field 0), location: in-object
    0000017100253599: [String] in OldSpace: #y: 66 (const data field 1), location: in-object
 }
0000017100287A59: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x017100287a31 <Map[20](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0171001c4581 <Cell value= 1>
 - instance descriptors (own) #2: 0x0171000ca08d <DescriptorArray[2]>
 - prototype: 0x017100244215 <Object map = 00000171002821E9>
 - constructor: 0x017100243e29 <JSFunction Object (sfi = 00000171001DB6D9)>
 - dependent code: 0x0171000021e9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0
DebugPrint: 00000171000CA0B5: [JS_OBJECT_TYPE]
 - map: 0x017100287a59 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x017100244215 <Object map = 00000171002821E9>
 - elements: 0x017100002261 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x017100002261 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000017100253589: [String] in OldSpace: #x: 7 (const data field 0), location: in-object
    0000017100253599: [String] in OldSpace: #y: 77 (const data field 1), location: in-object
 }
0000017100287A59: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x017100287a31 <Map[20](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0171001c4581 <Cell value= 1>
 - instance descriptors (own) #2: 0x0171000ca08d <DescriptorArray[2]>
 - prototype: 0x017100244215 <Object map = 00000171002821E9>
 - constructor: 0x017100243e29 <JSFunction Object (sfi = 00000171001DB6D9)>
 - dependent code: 0x0171000021e9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0



我们可以看到:打印出来的 kaimo666kaimo777 对象,它们的 map 属性都指向了同一个地址 0x017100287a59,说明它们共用了同一个 map。


f09bbcf1e75d4562be5e821e7c1fa7ac.png


实战3:重新构建隐藏类


给一个对象添加新的属性,删除新的属性,或者改变某个属性的数据类型都会改变这个对象的形状,那么就会触发 V8 为改变形状后的对象重建新的隐藏类。


kaimo.js 替换下面代码:


let kaimo = {};
%DebugPrint(kaimo);
kaimo.x = 666;
%DebugPrint(kaimo);
kaimo.y = 666;
%DebugPrint(kaimo);


执行下面命令:

v8-debug --allow-natives-syntax kaimo.js



结果如下:

DebugPrint: 0000006C000CA045: [JS_OBJECT_TYPE]
 - map: 0x006c00282301 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x006c00244215 <Object map = 0000006C002821E9>
 - elements: 0x006c00002261 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x006c00002261 <FixedArray[0]>
 - All own properties (excluding elements): {}
DebugPrint: 0000006C000CA045: [JS_OBJECT_TYPE]
 - map: 0x006c00287a31 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x006c00244215 <Object map = 0000006C002821E9>
 - elements: 0x006c00002261 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x006c00002261 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000006C002535A1: [String] in OldSpace: #x: 666 (const data field 0), location: in-object
 }
DebugPrint: 0000006C000CA045: [JS_OBJECT_TYPE]
 - map: 0x006c00287a59 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x006c00244215 <Object map = 0000006C002821E9>
 - elements: 0x006c00002261 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x006c00002261 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000006C002535A1: [String] in OldSpace: #x: 666 (const data field 0), location: in-object
    0000006C002535B1: [String] in OldSpace: #y: 666 (const data field 1), location: in-object
 }


可以看到,3个 map 都是不一样的,分别是 0x006c002823010x006c00287a310x006c00287a59

再来看看删除的情况,在 kaimo.js 替换成下面的代码:

let kaimo = {
    x: 666,
    y: 777
};
%DebugPrint(kaimo);
delete kaimo.x;
%DebugPrint(kaimo);


这里的 delete kaimo.x; 记得加分号,不然会报错:

6bca9649b6ef49608b850377c29a2777.png


执行下面命令:

v8-debug --allow-natives-syntax kaimo.js


结果如下,我们可以看到 map 也不一样了,分别是 0x006000287a590x006000285709,如果你删除了对象的某个属性,对象的形状也就随着发生了改变,这时 V8 也会重建该对象的隐藏类。

DebugPrint: 00000060000CA03D: [JS_OBJECT_TYPE]
 - map: 0x006000287a59 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x006000244215 <Object map = 00000060002821E9>
 - elements: 0x006000002261 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x006000002261 <FixedArray[0]>
 - All own properties (excluding elements): {
    0000006000253589: [String] in OldSpace: #x: 666 (const data field 0), location: in-object
    0000006000253599: [String] in OldSpace: #y: 777 (const data field 1), location: in-object
 }
DebugPrint: 00000060000CA03D: [JS_OBJECT_TYPE]
 - map: 0x006000285709 <Map[12](HOLEY_ELEMENTS)> [DictionaryProperties]
 - prototype: 0x006000244215 <Object map = 00000060002821E9>
 - elements: 0x006000002261 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x0060000ca095 <NameDictionary[29]>
 - All own properties (excluding elements): {
   y: 777 (data, dict_index: 2, attrs: [WEC])
 }




利用隐藏类性能优化


避免进行重新构建隐藏类的方法:


  1. 使用字面量初始化对象时,要保证属性的顺序是一致的。
  2. 尽量使用字面量一次性初始化完整对象属性。
  3. 尽量避免使用 delete 方法。





目录
相关文章
|
10天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
12天前
|
存储 Java 程序员
结构体和类的内存管理方式在不同编程语言中的表现有何异同?
不同编程语言中结构体和类的内存管理方式既有相似之处,又有各自的特点。了解这些异同点有助于开发者在不同的编程语言中更有效地使用结构体和类来进行编程,合理地管理内存,提高程序的性能和可靠性。
21 3
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
14天前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
14天前
|
存储 缓存 算法
结构体和类在内存管理方面有哪些具体差异?
【10月更文挑战第30天】结构体和类在内存管理方面的差异决定了它们在不同的应用场景下各有优劣。在实际编程中,需要根据具体的需求和性能要求来合理选择使用结构体还是类。
|
1月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1月前
|
存储 Java
深入理解java对象的内存布局
这篇文章深入探讨了Java对象在HotSpot虚拟机中的内存布局,包括对象头、实例数据和对齐填充三个部分,以及对象头中包含的运行时数据和类型指针等详细信息。
28 0
深入理解java对象的内存布局
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(二)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
1月前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。