图解 Google V8 # 04:V8 中的对象表示:怎么利用 Chrome 内存快照去查看对象在内存中是如何布局的?

简介: 图解 Google V8 # 04:V8 中的对象表示:怎么利用 Chrome 内存快照去查看对象在内存中是如何布局的?

说明

图解 Google V8 学习笔记



在 Chrome 中查看内存快照


1、首先我们 f12 在控制台运行下面这段程序

function Student(name, gender) {
  this.name = name;
  this.gender= gender;
}
var kaimo = new Student('kaimo', '男');


5c91260dc96b4c648396a60a065b8d75.png


2、切换到 Memory 中,点击左侧的小圆圈就可以捕获当前的内存快照。点开快照后,在过滤器中输入 Student,即可找到

2ef7fa2ebf7f48008ee700c4b8cfc2d8.png


V8 中对象的结构


在 V8 中,对象主要由三个指针构成,分别是隐藏类(Hidden Class)Property 还有 Element


  • 隐藏类用于描述对象的结构。
  • Property 和 Element 用于存放对象的属性,它们的区别主要体现在键名能否被索引。


b791e6daab2746b59c10ec5d1e34d4a4.png

命名属性的不同存储方式


V8 中命名属性有三种的不同存储方式:对象内属性(in-object)、快属性(fast)和慢属性(slow)。

  • 对象内属性保存在对象本身,提供最快的访问速度。
  • 快属性比对象内属性多了一次寻址时间。
  • 慢属性与前面的两种属性相比,会将属性的完整结构存储,速度最慢。


e9142208f19142418fefd5013db8c94e.png



隐藏类

上面提到的描述命名属性是怎么存放的,在 V8 中被称为 Map,更出名的称呼是 隐藏类(Hidden Class)


在 SpiderMonkey (火狐引擎)中,类似的设计被称为 Shape


为什么要引入隐藏类?


1、进行访问更快


通过哈希表的方式存取属性,需要额外的哈希计算。为了提高对象属性的访问速度,实现对象属性的快速存取,V8 中引入了隐藏类。



2、节省了内存空间:


在 ECMAScript 中,对象属性的 Attribute 被描述为以下结构。


   [[Value]]:属性的值

   [[Writable]]:定义属性是否可写(即是否能被重新分配)

   [[Enumerable]]:定义属性是否可枚举

   [[Configurable]]:定义属性是否可配置(删除)



fd2b3344f363412d93e914216d308f80.png



因为一般情况下,对象的 Value 是经常会发生变动的,而 Attribute 是几乎不怎么会变的。隐藏类的引入,将属性的 Value 与其它 Attribute 分开。这样的话可以减少内存的浪费。



隐藏类的创建

对象创建过程中,每添加一个命名属性,都会对应一个生成一个新的隐藏类。在 V8 的底层实现了一个将隐藏类连接起来的转换树,如果以相同的顺序添加相同的属性,转换树会保证最后得到相同的隐藏类。

具体可以看下面【实践3】的例子。



实践1:可索引属性和命名属性的存放

我们先去控制台执行下面代码


function Foo1 () {}
var a = new Foo1()
var b = new Foo1()
a.name = 'aaa'
a.text = 'aaa'
b.name = 'bbb'
b.text = 'bbb'
a[1] = 'aaa'
a[2] = 'aaa'



上面代码中,a、b 都有命名属性 name 和 text,另外 a 还额外多了两个可索引属性。

打开快照可以明显的看到,可索引属性是存放在 elements 中的。


V8 还为每个对象实现了 map 属性和 __proto__属性。

   __proto__ 属性就是原型,是用来实现 JavaScript 继承的。

   map 则是隐藏类


aa8111baa3414bd9a18f479297e6d182.png


我们在上面的代码中再加入一行:

a[1111] = 'aaa'

打开快照可以看到此时隐藏类发生了变化,Element 中的数据存放也变得没有规律了。


203612a201704460909f193df43514e3.png

原因:当我们添加了 a[1111] 之后,数组会变成稀疏数组。为了节省空间,稀疏数组会转换为哈希存储的方式,而不再是用一个完整的数组描述这块空间的存储。


哈希存储亦称“散列存储”,专用于集合结构的一种存储方式。


   数据元素存放在一块连续的存储区域中。数据元素的存放位置是通过一个哈希函数计算而得的。哈希函数将数据元素作为自变量,计算得到的函数值是数据元素的存储地址。



实践2:三种不同类型的 Property 存储模式


我们先去控制台执行下面代码

function Foo2() {}
var a = new Foo2()
var b = new Foo2()
var c = new Foo2()
for (var i = 0; i < 10; i ++) {
  a[new Array(i+2).join('a')] = 'aaa'
}
for (var i = 0; i < 12; i ++) {
  b[new Array(i+2).join('b')] = 'bbb'
}
for (var i = 0; i < 30; i ++) {
  c[new Array(i+2).join('c')] = 'ccc'
}


a、b 和 c 分别拥有 10 个,12 个和 30 个属性


对象内属性

先看 a 有10 个属性,对象内属性是在对象创建时就固定分配的,空间有限,数量固定为十个,空间大小相同(可以理解为十个指针)。


d8338a2b728445a69a36adc52dd583b2.png



快属性

然后看 b 有 12 个属性,当对象内属性放满之后,会以快属性的方式,在 properties 下按创建顺序存放。相较于对象内属性,快属性需要额外多一次 properties 的寻址时间,之后便是与对象内属性一致的线性查找。

564cb176f1e24cc18afd9d0c1f30b789.png


慢属性

最后看 c 有 30 个属性,和 b (快属性)相比,properties 中的索引变成了毫无规律的数,意味着这个对象已经变成了哈希存取结构了。


5a2ec46968be47d98bb567955d023008.png



实践3:增加属性对隐藏类的影响

我们先依次执行下面的例子:

f77eb9307b1341f095aaf6414616e32a.png


先执行:


function student(){}
let s = new student()



85d824f063a9434c955bad4075f91c8b.png


再执行:

s.name = 'kaimo'


1da1afc83dd94ddf96edf9e20a8c25c7.png

最后执行:

s.text = '男'


79d2ad66a6ed44d1917686a37b923892.png


我们从上面可以清晰的看到 s 在空对象时、添加 name 属性后、添加 gender 属性后会分别对应不同的隐藏类。

大致示意图如下:里面 Offset 不懂的可以参考下面文章:



a377c2f138ed45b1b5fcc561d650ab78.png



并且我们可以从内存快照中看到 Hidden Class2 的 back_pointer 指针指向 Hidden Class1。


f66f49a023d4453c8d63e3478e2c4ec0.png


隐藏类创建时的优化

例子:下面代码 a 和 b 的区别是,a 首先创建一个空对象,然后给这个对象新增一个命名属性 name。而 b 中直接创建了一个含有命名属性 name 的对象。

let a = {};
a.name = 'kaimo'
let b = { name: 'kaimo313' }



a 和 b 的隐藏类不一样,back_pointer 也不一样。因为在创建 b 的隐藏类时,省略了为空对象单独创建隐藏类的一步。


要生成相同的隐藏类,更为准确的描述是 —— 从相同的起点,以相同的顺序,添加结构相同的属性(除 Value 外,属性的 Attribute 一致)。



实践4:delete 操作对隐藏类的影响


按照添加属性的顺序删除属性

现在控制台执行下面代码:


function Foo4 () {}
var a = new Foo4()
var b = new Foo4()
for (var i = 1; i < 8; i ++) {
  a[new Array(i+1).join('a')] = 'aaa'
  b[new Array(i+1).join('b')] = 'bbb'
}


内存快照如下


1e1d70294a0c401f8b8b9cd8d7deeaf7.png

然后执行删除操作:

delete a.a

67e7d17a39db4361806c9fe18f23247b.png


查看内存快照如下:


d266536fd7204b41bf952a9c226f1f6e.png


删除了 a.a 后,a 变成了慢属性,退回哈希存储了。

按照添加属性的顺序逆向删除属性

在控制台执行下面代码

function Foo5() {}
var a = new Foo5()
var b = new Foo5()
a.name = 'kaimo'
a.gender = '男'
a.age = '8'
b.name = 'kaimo'
b.gender = '男'


内存快照如下:


a6d07d6e6c964619bd587b62453579d7.png


然后再执行删除

delete a.age


714b6e8bd2094280b580789b3837a78d.png


内存快照如下:

b8141ab182304f719529a9137274b2f8.png


我们发现删除了 a.age 后,a 和 b 的隐藏类相同,a 也没有退回哈希存储。



为什么说在 JS 中要避免使用 delete?


   delete 很多时候删不掉。


   delete 返回true的时候,也不代表一定删除成功。 比如原型上的属性。


   delete 某些场景下会导致隐藏类改变,可能导致性能问题。


因为对象的形状是可以被改变的,如果某个对象的形状改变了,隐藏类也会随着改变,这意味着 V8 要为新改变的对象重新构建新的隐藏类。而 delete 方法会破坏对象的形状,同样会导致 V8 为该对象重新生成新的隐藏类。我们应尽量避免使用 delete 方法。



目录
相关文章
|
21天前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
32 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
15天前
|
机器学习/深度学习 人工智能 缓存
【AI系统】推理内存布局
本文介绍了CPU和GPU的基础内存知识,NCHWX内存排布格式,以及MNN推理引擎如何通过数据内存重新排布进行内核优化,特别是针对WinoGrad卷积计算的优化方法,通过NC4HW4数据格式重排,有效利用了SIMD指令集特性,减少了cache miss,提高了计算效率。
35 3
|
19天前
|
缓存 监控 算法
Python内存管理:掌握对象的生命周期与垃圾回收机制####
本文深入探讨了Python中的内存管理机制,特别是对象的生命周期和垃圾回收过程。通过理解引用计数、标记-清除及分代收集等核心概念,帮助开发者优化程序性能,避免内存泄漏。 ####
31 3
|
25天前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
39 3
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
89 4
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
80 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
261 9
|
2月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
37 2
|
2月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
60 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
28天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
232 1