前言记录一下V8中基本类型和对象的存储方式。js的数据类型js的数据大致分为两种,一种是原始类型(Boolean、Null、Undefined、Number、BigInt、String、Symbol),一种是对象(Object)。原始数据放在栈上,对象数据放在堆上。栈和堆的区别是一个不连续的内存区域,即可以任意存放数据,主要存放对象。堆栈(stack)是一个连续的内存区域。每个块都按一定顺序存储(后进先出)。栈主要存放基本类型变量的值和指向数组或对象在堆中的地址。为什么要区分两种栈变量呢?一种内容较短(比如一个int整数),需要频繁访问,但它的生命周期较短,通常只在一个方法中存活,而另一种可能内容很多(比如很长的字符串),可能不需要太频繁的访问,但是生命周期长,通常用在很多方法中,所以把这两类变量分开比较合理,一类存放在栈区,通常是局部变量,运算符栈,函数参数传递和返回值,另一类存放在堆区,通常是大型结构体(或OOP中的对象),需要反复访问的全局变量。堆区各种慢,申请内存慢,访问慢,修改慢,释放慢,整理慢(或者GC垃圾回收),但是优点不言而喻,访问随机灵活,大空间,在不超过可用内存的情况下,想给多少就给多少。栈区就像一个临时工,干完活就跑了,所以超级快,但是也有很多缺点,比如生命周期短,一般只能在一个method中生存,比如你需要提前知道你需要多少栈(其实是很大的)大部分语言要分配的栈区大小是在编译时确定的,Java就是这样),通常是栈区的最大可用内存很小,你不可能往栈区堆很多数据。原始类型原始类型的一个特征是它们是不可变的。示例代码如下//例1varstr="abc";str[0]="d";console.log(str)//abc//例2varstr2="abc";str2="dbc";console.log(str)//dbc示例1中的数据没有变化,但是示例2中的数据发生了变化。其实例子2新建了一个字符串,也就是为“dbc”开辟了一块新的内存区域。简单的说,就是假设栈中存了一个数据如“abc”,那么这个数据永远不会改变,如果像例子2那样赋值另一个字符串或者其他任何改变的值,原来的“abc”会保留在栈中,并会开辟一个新的地方存放“dbc”。类似于下图:为什么要把底层类型的值设置为不可变的?可以安全地假设底层类型的值是可变的,那么下面的代码就会变得很奇怪varstrTest="varaiable";varfun=(str)=>{str+"---ok"};fun(strTest);console.log(strTest)//varaiable---ok//可以看到strTest的值已经改变了,尤其是像map这样的对象varmap=newMap()varstrTest="t1";map.set(strTest,10);strTest="notT1";map.get("t1");//undefined;map.get("notT1");//10这样的代码很可能会导致更多的bug,尤其是像java这样的多线程语言,更容易导致线程不安全的问题。实际上,为了共享,在基本类型中,具有相同值的变量共享一块内存区域。这样做的好处是可以避免额外的内存开销,提高性能。当然,这个前提是底层类型是不可变的,否则,如果str1的值发生变化,str2的值也会发生变化(实际上并没有对其进行任何操作)。对象类型V8中对象(数组也是对象)的存储比较复杂,是堆中存储的数据。并且格式大致如下:这个和很多资料说的用Map实现的不一样。显然,根据上图(来自v8博客),至少可以说明没有用Map处理。V8将对象中的属性分为两类,一类是字符常量,一类是数字或数字字符串(比如“1”),分别放在Properties和Elements两个数组中。普通字符常量先从普通字符常量说起,字符常量的存储方式又细分为三类。第一类:In-object其实在生成对象的时候,v8会留出一些空间给对象分配属性(数量由对象的初始大小预先决定),这些属性直接存储在对象本身.这些是V8中最快的属性,因为不需要任何间接访问就可以访问它们,如下图:第二类:快速属性v8的In-object空间并不多,创建的非属性对象对象字面量被分配了4个对象内属性存储(inobject_properties)空间。当这些空间用完后,会使用HideClass(隐藏类,有的也叫Maps,这里统称为隐藏类)辅助快速访问属性。HiddenClasses和DescriptorArraysHiddenClass存储关于对象的元信息,包括该对象的属性数量和对对象原型的引用。另外,在HiddenClasses中还有一个DescriptorArrays数组,存放的是对象属性的信息。也就是如下图所示:这里一般会有一个疑问,为什么我们需要一个隐藏类,我直接创建一个hashTable不是更快吗?关于隐藏类和IC的概率,推荐阅读这篇JavaScript引擎基础:Shapes和InlineCaches,概念清晰易懂,图文并茂。这里简单介绍一下概念:首先,我们看一下隐藏类是怎么来的。从图中可以看出,隐藏类是通过一棵树不断生成的。每增加一个属性,都会生成一个新的隐藏类节点(添加数组索引属性不创建新的),那么,具有相同结构(相同属性,相同顺序)的对象具有相同的隐藏类。也就是说如果在上面的代码中加入一段代码如下:vara={};a.a="ddd";变量b={};b.a="3";b.b="测试";那么a的隐藏类就是右边第一个nofOwnDescriptors,b就是第二个。对于程序代码来说,很多对象其实都有同一个隐藏类。隐藏类背后的主要动机是内联缓存或IC的概念。IC是让JavaScript变快的关键!JavaScript引擎使用IC来记住在哪里寻找对象属性以减少昂贵的查找次数。大致来说,每次代码编译成字节码,读取属性时,都会根据隐藏类保存属性的位置。下次读取或者遇到隐藏类相同的对象时,可以按照隐藏类进行读取。直接读取提供的属性位置,避免查找过程。第三类:慢属性最后一种方法是字典存储法。字典存储方式比较简单,先看官方图:简单的说,就是将隐藏类中的DescriptorArrays直接设置为空,然后在properties中直接存储属性的值和元信息数组,并通过Hash方式获取和设置。既然上面说了有隐藏类可以提高性能,那为什么还要提供字典方法呢?v8的原文如下:但是,如果一个对象中添加和删除的属性很多,会产生大量的时间和内存开销来维护描述符数组,而HiddenClasses大致意思是添加和删除过多的属性将使用维护descriptorArray和HiddenClasses需要大量时间和内存开销。最后,什么时候是Fast属性(隐藏类),什么时候是slow属性(字典模式)?关于这方面,我推荐七级小雪学V8系列文章之一,对象访问方式优化,下面部分是参考了新创建的小对象是Fast属性。当执行以下操作时,它会变成缓慢的属性。动态添加太多属性。删除属性(delete)删除非最后添加的属性(V8>=6.0)。数组类型有很多种。据官方说法,多达20种。.其实数组一般是放在开头提到的elements数组中,然后根据索引读取值。这个比较简单,下面介绍两种比较典型的。如果缺少元素,则根据原型链字符串取值,其实就是对象原型链..consto=['a','b','c'];控制台日志(o[1]);//打印'b'.deleteo[1];//在元素中引入一个空洞store.console.log(o[1]);//打印“未定义”;属性1不存在.o.__proto__={1:'B'};//在原型上定义属性1。安慰。日志(o[0]);//打印'a'。安慰。日志(o[1]);//打印'B'。控制台日志(o[2]);//打印'c'.console.log(o[3]);//打印undefinedsparsearray,如果出现这种情况,元素中会有大量未使用的内存,所以v8优化成字典模式,和上面的字符串一样。constsparseArray=[];sparseArray[9999]='foo';//创建一个包含字典元素的数组。此外,v8还对数组进行了各种优化,如Gc等,这里不再赘述。引用文章FastpropertiesinV8(官方文章)V8系列文章(推荐)JavaScript引擎基础:Shapes和InlineCaches(推荐)
