图解 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 方法。



目录
相关文章
|
17天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
121 9
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1月前
|
Web App开发 开发者
|
1月前
|
存储 Java
深入理解java对象的内存布局
这篇文章深入探讨了Java对象在HotSpot虚拟机中的内存布局,包括对象头、实例数据和对齐填充三个部分,以及对象头中包含的运行时数据和类型指针等详细信息。
28 0
深入理解java对象的内存布局
|
3月前
|
存储 算法 Oracle
不好意思!耽误你的十分钟,JVM内存布局还给你
先赞后看,南哥助你Java进阶一大半在2006年加州旧金山的JavaOne大会上,一个由顶级Java开发者组成的周年性研讨会,公司突然宣布将开放Java的源代码。于是,下一年顶级项目OpenJDK诞生。Java生态发展被打开了新的大门,Java 7的G1垃圾回收器、Java 8的Lambda表达式和流API…大家好,我是南哥。一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
不好意思!耽误你的十分钟,JVM内存布局还给你
|
3月前
|
缓存 Java 编译器
Go 中的内存布局和分配原理
Go 中的内存布局和分配原理
|
2月前
crash —— 获取物理内存布局信息
crash —— 获取物理内存布局信息
|
3月前
|
存储 编译器 C++
Method&ConstMethod的内存布局
综上所述,常规方法和常量方法在对象的内存布局中并不直接占据空间;它们作为代码的一部分存储在程序的代码段中。对于虚方法(包括常量虚方法),它们通过VTable在对象中有表示,但即便在这种情况下,方法代码本身也不在对象的内存布局中。理解这些概念有助于深入理解面向对象编程,提高编程效率和代码的可理解性。
37 3
|
4月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。
125 8
|
3月前
|
存储 NoSQL 程序员
C语言中的内存布局
C语言中的内存布局
46 0