内存储存机制
数据类型
JavaScript 中的主要数据类型有 Boolean、Number、String、Symbol、Null、Undefined 和 Object
其中最后一种是引用类型,其他的全是原始类型,如何把它们区分为两种不同的类型呢,按照它们在浏览器内存中存放的位置来区分即可。
浏览器内存空间
浏览器内存空间由三部分组成,分别是:
- 代码空间:存储可执行代码
- 调用栈空间:存储执行上下文,包括指向堆空间的指针
- 堆空间:存放对象类型的数据
为什么要设计堆空间和栈空间
JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,这就需要确保栈空间不能太大,才能快速切换上下文,进而保证整个程序的执行效率。
通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据,堆空间很大,能存放很多大的数据。
数据类型的区别
除了存储方式不同外,数据类型的赋值操作也有区别,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。
注意,函数也是引用类型的一种,函数的内存模型由三部分组成:执行上下文、this 指针、作用域链。其中执行上下文和作用域链由函数的具体代码决定,在编译阶段就确定了、而 this 由调用函数方决定,在执行阶段才能确定。
函数的内存空间
当创建一个函数时,实际上是创建了一个函数对象。函数对象本身、函数的代码、函数对象相关属性(名称、参数的数量)、作用域链信息以及对函数代码的引用也存储在堆空间中。
当函数被调用时,会创建一个执行上下文。这个执行上下文包括变量对象、作用域链和 this 的值。执行上下文的一部分信息会存储在占空间中,用于函数执行期间的变量访问和操作。
函数执行过程中访问的函数对象本身以及其中的函数代码等信息仍然是从堆空间中获取的。
当函数执行完毕后,栈空间中的执行上下文会被销毁(释放栈空间),但堆空间中的函数对象和函数代码仍然存在,直到所有指向该函数的指针都被释放后,JavaScript 的垃圾回收机制才会回收这些堆空间。
内存回收机制
- 内存回收机制是运行在主线程之上,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。
- 要理解调用栈的回收机制,我们需要引入一个指针,一个记录当前执行状态的指针,这个指针指向调用栈中当前正在执行的函数,一般情况下都是指向栈顶的函数。当栈顶函数执行完毕,指针便会向下移动,这个下移操作就是销毁顶部函数执行上下文的过程。
- 堆空间中的垃圾数据,是由垃圾回收器来自动释放的。
JavaScript 中的垃圾回收器
要理解 JavaScript 中的垃圾回收器机制,我们需要把数据根据存活时间分成两类,一类存在时间很短,一类存在时间很长。
V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的内存占用小的对象,老生代中存放的生存时间长的对象和内存占用大的对象。
- 新生代的垃圾回收,主要由副垃圾回收器负责。
- 老生代的垃圾回收,主要由主垃圾回收器负责。
垃圾回收器的工作流程
- 区分空间中还在使用的对象和可以进行垃圾回收的对象,分别标记为活动对象和非活动对象。
- 回收非活动对象所占据的内存。
- 整理内存,将多次回收对象后产生的不连续的内存空间合并成连续的内存空间。
副垃圾回收器的工作机制
- 空间分配:使用 Scavenge 算法,把新生代空间对半划分为对象区域和空闲区域。新加入的所有对象都会存放在对象区域。为了执行效率,新生代整体空间不大。
- 触发机制:对象区域快被写满时。
- 工作机制:
- 标记对象区域中的活动对象和非活动对象,直到所有数据都被标记完成。
- 所有活动对象被复制到空闲区域并有序地排列起来,完成了内存整理操作。
- 将空闲区域和对象区域进行角色翻转,并清空空闲区域的数据,这样就完成了非活动对象的回收操作。
- 空间利用:一般空间不会随着页面逻辑进行而增大,新生代的空间借助于区域角色翻转的操作,能无限重复使用下去。
- 对象晋升策略:经过两次垃圾回收依然还存活的对象,会被移动到老生代中。
主垃圾回收器的工作机制
- 对象分配:除了新生代中晋升的对象,一些大的对象会直接被分配到老生代。
- 触发机制:在各个 JavaScript 任务中间执行,以降低垃圾回收带来的交互影响。
- 工作机制:
- 清理一阶段:采用
标记-清除法,遍历执行栈中的根元素与根元素的引用,在遍历过程中遇到的元素标记为活动对象,遍历完成后,未被标记上的元素都判断为垃圾数据,再在清理过程将这些垃圾数据全部清除。 - 清理二阶段:多次采用
标记-整理法,会产生大量不连续的内存碎片,这时需要依赖标记-整理算法,首先一样是标记活动对象,不一样的是后续操作,这个方法的后续是让所有存活的对象都向一端移动,然后由清理掉端边界以外的内存。 - 为了降低老生代的垃圾回收造成的卡段,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记算法。
- 清理一阶段:采用
- 交互体验:一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。