文档
在V8引擎中,JavaScript的底层数据结构被精心设计和优化,以确保代码能够提高速度运行,并尽可能节省内存。我们可以把V8看作是一台非常聪明的机器,它能够自动调整和优化数据存储的方式,让JavaScript代码运行得又快又流畅。下面我会用一些生动的比喻来解释V8中这些数据结构是如何工作的。
1. 对象和属性存储:像搭积木一样
在V8中,堆
是内存分配的核心区域,存储了JavaScript对象、字符串、闭包等。
在V8中,JavaScript对象就像是用积木搭建的建筑。每一个对象都是由许多小块(属性)组成的,而这些小块的排列方式非常重要。为了高效地管理这些积木块,V8引入了一个概念,叫做隐式类(Hidden Classes)
,可以把它想象成一张“建筑图纸”。
堆(Heap)
在V8中,堆
是内存分配的核心区域,存储了JavaScript对象、字符串、闭包等。V8的堆分为几个不同的区域,每个区域负责存储特定类型的数据。
- 新生代(Young Generation) :这个区域用来存储生命周期较短的对象。新生代堆分为两个部分:To空间和From空间。新创建的对象首先被分配在To空间,当垃圾回收发生时,存活的对象会被复制到From空间,而To空间会被清空。
- 老生代(Old Generation) :生命周期较长的对象会被移动到老生代堆,这里的对象通常经历了几次垃圾回收仍然存活。老生代堆进一步分为
Old Pointer Space
和Old Data Space
,分别存储指针数据和非指针数据(如字符串和数组的内容)。
隐式类(Hidden Classes)
- 当你创建一个新的对象时,V8会为这个对象生成一张图纸。这张图纸告诉V8,这个对象有哪些属性(积木块),它们是如何排列的。
- 如果你在对象中添加或删除属性,V8会生成一张新的图纸。这些图纸帮助V8快速找到和操作这些属性,避免每次都要从头检查一遍。
V8在处理JavaScript对象的属性存储时,经常使用哈希表
来管理对象的键值对,特别是在对象结构动态变化或者属性数目较多时。
属性存储方式
-
快速属性(Fast Properties) :如果你的建筑(对象)比较简单,只有少量积木块(属性),V8会把这些积木块紧紧地排在一起,就像用胶水粘好的一堆积木,这样可以很快找到你需要的那个。在对象结构简单且属性数目较少时,V8将属性存储在一个连续的内存块中,这种方式通过偏移量直接访问属性,速度非常快。
-
字典模式(Dictionary Mode) :如果你的建筑开始变得复杂,积木块越来越多,或者你经常改变它们的位置,V8会把这些积木块放进一个“字典”里。虽然在字典里找东西可能会慢一些,但它能更好地适应复杂的建筑。每个键值对被存储在哈希表的桶中,并通过哈希函数计算键在表中的位置。字典模式牺牲了一定的访问速度,但提供了更大的灵活性。
字典结构
- 键值对存储:字典以键值对的形式存储属性,并且通常采用开放地址法或链地址法来解决哈希冲突。
- 属性访问:在字典模式下,属性的查找速度比直接的偏移量计算慢,但它允许更动态的对象结构。
2. 数组存储:密集的排列和稀疏的分布
JavaScript数组在V8中存储的方式可以根据内容的不同而有所变化。想象一下你在整理一个书架,这个书架可以按照书的排列方式分为两种情况:
密集数组(Dense Arrays)
- 如果你的书架上摆满了书,而且书的顺序是连续的(比如第1本书、第2本书…),V8会直接为这些书分配一整块空间,就像一条长长的书架。这种方式非常高效,因为你可以很快找到第几本书。
- 元素存储:当数组是密集的,即元素的索引是连续的,V8会使用一块连续的内存区域来存储数组的元素。这种方式的访问速度非常快,因为可以通过简单的偏移量计算来定位元素。
稀疏数组(Sparse Arrays)
- 但是如果书架上只有零星几本书,或者书的位置很不规则(比如第1本书、第100本书…),V8会使用一种特殊的存储方式,将书的位置和内容记录下来,就像一本索引表,这样虽然找书的过程可能会慢一些,但能够更好地处理这些稀疏的情况。
- 稀疏数组存储:对于稀疏数组,即元素索引不连续,V8不会为每个元素都分配内存,而是使用一个哈希表或其他类似的数据结构来存储非空元素的索引和对应的值。这种存储方式减少了内存的浪费,但访问速度会比密集数组慢一些。
元素种类优化
- 类型特化:V8会尝试根据数组中元素的类型来选择最合适的数据结构。如果数组中的所有元素都是整数,V8可能会使用一种更紧凑的表示方式来存储这些元素。
3. 字符串存储:节省空间的“魔法”
字符串在V8中存储时,V8采用了多种“魔法”来节省空间和提升性能。可以把它想象成一本书,它有时候可能是原版,有时候是复印版,甚至有时候是拼接出来的。
普通字符串
- 这就是一本完整的书,所有的内容都已经写好并存放在内存中。
切片字符串(Sliced Strings)
- 如果你只需要这本书的某一部分,V8不会去复印那部分内容,而是做一个“书签”,标记你需要的章节,这样可以避免浪费内存。
拼接字符串(Concatenated Strings)
- 如果你通过JavaScript代码把几个字符串拼接起来,比如用
+
号(比如substring
),V8会把它们“借来”拼成一个临时的字符串,而不是真的创建一个新的字符串。这种做法也可以节省内存。
4. 垃圾回收:清理工人的工作
在V8中,垃圾回收器就像是你家里的清洁工,负责定期打扫和清理不再使用的东西,以腾出更多的空间来存放新的物品。V8采用了一些聪明的策略来高效地完成这个任务。
标记-清除(Mark-and-Sweep)
- 清洁工会先用记号笔标记所有仍在使用的物品(对象),然后将没有标记的物品清除掉,腾出空间。这个过程就像你先把需要的东西放在一边,再把不用的东西扔掉。
标记-压缩(Mark-and-Compact)
- 为了解决内存碎片化问题,V8还会将存活的对象压缩到堆的一端,释放出连续的内存块。
增量垃圾回收(Incremental Garbage Collection)
- 为了避免打扫过程中影响到你正在做的事情(比如正在浏览网站),清洁工不会一次性把所有房间都打扫完,而是分阶段进行,每次只打扫一点点,这样你几乎感觉不到打扫的存在。
5. 即时编译(JIT):让机器码飞起来
V8中的即时编译(JIT)就像是一位超级翻译员,他能够在你说话的同时,立刻把你的话翻译成机器能听懂的语言(机器码),并且会记住你经常说的内容,以后就可以更快地翻译出来。
内联缓存(Inline Caching)
- 这位翻译员会根据你过去的对话,猜测你接下来可能会说什么。如果你经常说类似的话,他就会在手边准备好这些翻译好的词句,等你一开口就能快速回答。
- 内联缓存是 V8 优化属性访问和方法调用的重要技术。通过缓存对象的隐藏类和属性访问信息,V8 能够避免重复执行相同的查找操作,从而提高代码执行速度。
抽象语法树(AST)和字节码
- 抽象语法树:在解析JavaScript代码时,V8首先会生成一个抽象语法树(
AST
),它描述了代码的结构。 - 字节码和优化代码:V8会将
AST
转换为字节码,并在执行过程中进行优化。为此,V8使用控制流图(CFG
)等数据结构来分析代码的执行路径,进行内联、循环展开等优化。
栈帧结构
栈是V8中用于管理函数调用的基本数据结构。每次函数调用时,都会在栈上分配一个新的栈帧,存储函数的参数、局部变量以及返回地址。
- 栈帧:每个栈帧包括调用者的信息、局部变量的存储、以及返回值的存储位置。当函数返回时,栈帧被销毁,并恢复到先前的状态。
- 递归和深度控制:V8会通过栈帧来管理递归调用,并控制递归的深度以防止栈溢出。
6. Typed Arrays 和 WebAssembly:为数据而生的存储方式
V8不仅支持JavaScript的标准数据结构,还支持一些特别为处理大量数据而设计的存储方式,像是Typed Arrays
和WebAssembly
。
Typed Arrays
- 这就像是专门的书架,用来存放同一类型的书籍,比如一本全是数字的书。这种书架排列紧凑,取书非常快,非常适合存放和处理大量的数据。
- 二进制数组:Typed Arrays是一种特殊的数组类型,用于存储原始二进制数据,比如整数、浮点数。这些数组在内存中是连续存储的,并且与底层硬件的内存模型高度一致,使得它们非常适合高性能计算任务。
WebAssembly
WebAssembly
像是给V8装上了一个“外挂”,允许你直接运行二进制格式的代码,这种代码运行得更快,非常适合高性能的应用,比如3D游戏或者视频处理。- 线性内存:
WebAssembly
模块中的内存是线性的,类似于C语言中的数组。它允许在WebAssembly
模块和JavaScript之间高效地共享和操作二进制数据。
通过这些巧妙的设计和优化,V8能够高效地管理JavaScript的各种数据结构,确保代码在执行时既快又省内存。它就像一个智能的管家,时刻关注着你的需求,并动态调整存储和执行方式,让一切都运行得井井有条。