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



目录
相关文章
|
2月前
|
Web App开发
在 HTML 中禁用 Chrome 浏览器的 Google 翻译功能
在 html 标签中添加 translate=“no” 属性,浏览器将不会翻译整个页面。
100 0
|
2月前
|
存储 安全 Java
synchronized原理-字节码分析、对象内存结构、锁升级过程、Monitor
本文分析的问题: 1. synchronized 字节码文件分析之 monitorenter、monitorexit 指令 2. 为什么任何一个Java对象都可以成为一把锁? 3. 对象的内存结构 4. 锁升级过程 (无锁、偏向锁、轻量级锁、重量级锁) 5. Monitor 是什么、源码查看(hotspot虚拟机源码) 6. JOL工具使用
|
2月前
|
缓存 Java
Java中循环创建String对象的内存管理分析
Java中循环创建String对象的内存管理分析
40 2
|
3天前
|
设计模式 缓存 Java
Java设计模式:享元模式实现高效对象共享与内存优化(十一)
Java设计模式:享元模式实现高效对象共享与内存优化(十一)
|
14天前
|
存储 缓存 JavaScript
JavaScript内存泄漏通常发生在对象不再需要时
【6月更文挑战第16天】JavaScript内存泄漏常由闭包引起,当不再需要的对象仍被闭包引用时,垃圾回收机制无法清理。例如,创建返回大型对象引用的闭包函数会导致内存泄漏。避免泄漏需及时解除引用,清除事件监听器,利用WeakMap或WeakSet,以及定期清理缓存。使用性能分析工具监控内存使用也有助于检测和解决问题。
26 8
|
10天前
|
算法 Java 程序员
Python内存管理用引用计数(对象的`ob_refcnt`)跟踪对象,但循环引用(如A-&gt;B-&gt;A)可导致内存泄漏。
【6月更文挑战第20天】Python内存管理用引用计数(对象的`ob_refcnt`)跟踪对象,但循环引用(如A-&gt;B-&gt;A)可导致内存泄漏。为解决此问题,Python使用`gc`模块检测并清理循环引用,可通过`gc.collect()`手动回收。此外,Python结合标记清除和分代回收策略,针对不同生命周期的对象优化垃圾回收效率,确保内存有效释放。
15 3
|
12天前
|
算法 Java
Java垃圾回收(Garbage Collection,GC)是Java虚拟机(JVM)的一种自动内存管理机制,用于在运行时自动回收不再使用的对象所占的内存空间
【6月更文挑战第18天】Java的GC自动回收内存,包括标记清除(产生碎片)、复制(效率低)、标记整理(兼顾连续性与效率)和分代收集(区分新生代和老年代,用不同算法优化)等策略。现代JVM通常采用分代收集,以平衡性能和内存利用率。
38 3
|
2天前
|
存储 缓存 Java
Java对象内存布局深度解析
Java对象内存布局深度解析
8 0
|
4天前
|
监控 算法 Java
掌握Java内存管理:对象生命周期与垃圾回收机制
本文旨在为读者提供一次深入的探索之旅,穿越Java虚拟机(JVM)的迷宫,揭示对象从诞生到消亡的奥秘。我们将一起揭开内存分配、存活判定以及回收策略等概念背后的神秘面纱,通过案例分析与实践技巧,让读者能够更加高效地运用Java语言,优化程序性能。
|
5天前
|
存储 缓存 算法
JVM对象创建与内存分配机制
该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
12 0

热门文章

最新文章