22

基本储存单元

位(bit):二进制数中的一个数位,可以是0或者1,是计算机中数据的最小单位。

字节(Byte,B):计算机中数据的基本单位,每8位组成一个字节。各种信息在计算机中存储、处理至少需要一个字节。例如,一个ASCII码用一个字节表示,一个汉字用两个字节表示。

字(Word):两个字节称为一个字。汉字的存储单位都是一个字。

扩展的存储单位

在计算机各种存储介质(例如内存、硬盘、光盘等)的存储容量表示中,用户所接触到的存储单位不是位、字节和字,而是KB、MB、GB等,但这不是新的存储单位,而是基于字节换算的。

KB:1KB=1024B
MB:1MB=1024KB
GB:1GB=1024MB
TB:1TB=1024GB

UTF-8编码:一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。中文标点占三个字节,英文标点占一个字节。

Unicode编码:一个英文等于两个字节,一个中文(含繁体)等于两个字节。中文标点占两个字节,英文标点占两个字节

数组

数组最大长度 2^32 – 1(即4294967296 – 1),js数组并不是真正意义上的数组(栈)

在计算机科学中,数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。

JavaScript的数组是否分配连续内存取决于数组成员的类型,如果统一是单一类型的数组那么会分配连续内存,如果数组内包括了各种各样的不同类型,那么则是非连续内存。

非连续内存的数组用的是类似哈希映射的方式存在,它可以通过多种数据结构实现,其中一种是链表。比如声明了一个数组,他被分配给了1001、2011、1088、1077四个非连续的内存地址,通过指针连接起来形成一个线性结构,那么当我们查询某元素的时候其实是需要遍历这个线性链表结构的,这十分消耗性能。

现在, JavaScript 引擎已经在为同种数据类型的数组分配连续的存储空间了。另外,在 ES2015/ES6 中, 数组还有其它改进。 TC39 决定在 JavaScript 中引入类型化数组,所以如今我们有 ArrayBuffer了。ArrayBuffer 会有一大块连续的存储位置,你能用它做任何你想做的事情。不过,直接处理内存涉及非常底层的操作,相当复杂。

字符串

字符串理论上的最大长度是没有限制的,只要你的js解释器内存够大,但是当前大多数浏览器都不能支持2^28(即268435456)位字符串。如果全部存储英文字符,则大小为268435456 / 1024 / 1024 = 256 MB

那么这么大的字符串在内存中是怎么存储的呢?
ECMAScript 5.1规范在“内存”的定义上非常模糊,并没有明确定义出ECMAScript实现中的各个运行时区域的划分,也就不存在“stack/heap划分”。所以,从定义层面看不出String存储的字符串是在stack还是heap上分配的。

ECMAScript规范只规定了String类型要是一个值类型,这个类型要可以存储UTF-16为单元的字符,完全没有规定String类型要如何实现。所以各个JavaScript引擎的具体实现就各显神通了。

V8在内存分配上面,对String直接使用堆存储,不用经过c++堆外分配内存,并且Google也对String进行优化,在实际的拼接测速对比中,String比Buffer快。

在 V8 中字符串有如下 5 种表达模式:
SeqString
已经要查看内容的字符串:使用flat string思路来实现,本质上说就是用数组形式来存储String的内容。
实际数据存储时分为 OneByte、TwoByte(Unicode)两类。

ConsString
拼接字符串但尚未查看其内容:在字符串拼接时,采用树形结构表达拼接后(first + second)的字符串。使用“rope”思路或其它延迟拼接的思路来实现。当需要查看其内容时则进行“扁平化”操作将其转换为flat string表现形式。最常见rope的内部节点就像二叉树一样,但也可以有采用更多叉树的设计的节点,或者是用更动态的多叉树实现。

SliceString(parent, offset)
子串(substring):在字符串切割时,采用 offset 与 [length] 表达父字符串(parent)的一部分。使用“slice”思路来实现,也就是说它只是一个view,自己并不存储字符内容而只是记录个offset和length,底下的存储共享自其引用的源字符串。

ThinString(actual)
值得驻留(intern)的字符串:在有些场景下会重复出现的字符串,当两个变量保存相同的字符串时,它们实际上是保存了这个字符串在内存中的地址。最大的好处是在特殊场景下有些字符串会经常重复出现,或者要经常用于相等性比较,把这些字符串驻留起来可以节省内存(内容相同的字符串只驻留一份),并且后续使用可以使用指针比较来代替完全的相等性比较(因为驻留的时候已经比较过了)。在多数情况下可以被认为与 ConsString(actual, empty_string) 等价。

ExternalString
外来字符串:代表了产生在 V8 堆外的字符串资源。有时候JavaScript引擎跟外界交互,外界想直接把一个char8_t或者char16_t传给JavaScript引擎当作JavaScript字符串用。JavaScript引擎可能会针对某些特殊场景提供一种包装方式来直接把这些外部传进来的字符串当作JavaScript String,而不拷贝其内容。

值得注意的是:虽然ECMAScript的String值是值类型的,这并不就是说“String值就是在栈上的”。正好相反,V8所实现的String值全部都是在V8的GC堆上存储的,传递String值时实际上传递的是指向它的指针。但由于JavaScript的String值是不可变的,所以底层实现无论是真的把String“放在栈上”还是传递指针,对上层应用的JavaScript代码而言都没有区别。

ExternalString虽然特殊但也不例外:它实际存储字符串内容的空间虽然是从外部传进来的,不在V8的GC堆里,但是ExternalString对象自身作为一个对象头还是在GC堆里的,所以该String类型实现逻辑上说还是在GC堆里。

v8中,Boolean、Null、Undefined、Number保存在栈内存中,这些类型的值都可以用32位的数据来表示。那么剩下的数据类型Symbol、BigInt、Object、String就都是要保存在堆中的,栈里面只会保存这些值的地址的引用。

栈内存:

1.存储的值大小固定
2.空间较小
3.可以直接操作其保存的变量,运行效率高
4.由系统自动分配存储空间

栈内存是一个线性的、规则的、大小基本固定的、有序的排列起来的一块块内存空间,每个单元大小固定,规则有序的排列下来

堆内存:

5.存储的值大小不定,可动态调整
6.空间较大,运行效率低
7.无法直接操作其内部存储,使用引用地址读取
8.通过代码进行分配空间

数字

Number,遵循 IEEE 754 规范,采用双精度存储(double precision),占用 64 位,其中1位用来表示符号位,11位用来表示指数,剩下52位表示尾数,就是这个Number.MAX_SAFE_INTEGER,又称为最大安全数,其值为9007199254740991,换算成二进制就是2^53-1,即占52位。。大于 9007199254740992 的可能会丢失精度

9007199254740992 + 1 // 丢失
9007199254740992 + 2 // 未丢失
9007199254740992 + 3 // 丢失
9007199254740992 + 4 // 未丢失

类似的,数字还有一个最小安全数Number.MIN_SAFE_INTEGER,其值为-9007199254740991

数字最大数为Number.MAX_VALUE,其值为1.7976931348623157e+308,介于2^1023 – 2^1024之间。大于的值MAX_VALUE表示为Infinity

ECMAScript 标准约定number数字需要被当成 64 位双精度浮点数处理,但事实上,一直使用 64 位去存储任何数字实际是非常低效的,所以 JavaScript 引擎并不总会使用 64 位去存储数字,引擎在内部采用其他内存表示方式(如 32 位),只要保证数字外部所有能被监测到的特性对齐 64 位的表现就行。

V8不仅仅是使用32位来表示数字那么简单,还对数字进行了分类,将数字分为了Smi 和 HeapNumber(这个仅仅是引擎层面的处理,js内部只认识数字,不区分整数和浮点数)。

V8中Smi代表的是小整数(-2^31,2^31-1),而HeapNumber则代表了一些浮点数以及无法用32位表示的数,比如NaN,Infinity,-0等。因为小整数在我们的编码过程中太常见了,所以,V8专门把它拿出来,并且对其进行了优化操作,这样它就可以进行快速整型操作,比如for循环等。

当我们更新他们的值的时候,Smi的值会原地更新,而HeapNumber由于它不可变的特性,V8会开辟一个新的内存实体用来储存新的值,如果我们需要频繁更新HeapNumber的值,执行效率会比Smi慢得多。

v8引擎与内存分配

V8 是谷歌开发的高性能 JavaScript 引擎,该引擎使用 C++ 开发。目前主要应用在 Google Chrome 浏览器和 node.js 当中。

V8 自带的高性能垃圾回收机制,使开发者能够专注于程序开发中,极大的提高开发者的编程效率。但是方便之余,也会出现一些对新手来说比较棘手的问题:进程内存暴涨,cpu 飙升,性能很差等。

一个 V8 进程的内存通常由以下几个块构成:

新生代内存区(new space)

64位下新生代的空间为64M,32位下新生代为16M。大多数的对象都会被分配在这里,这个区域很小但是垃圾回收比较频繁;

老生代内存区(old space)

64位下老生代为1400M;32位下老生代为700M。属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针;

大对象区(large object space)

这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区;

代码区(code space)

代码对象,会被分配在这里。唯一拥有执行权限的内存;

map 区(map space)

存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单。


hfhan
28.9k 声望27.4k 粉丝

砥砺前行