在JavaScript的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间。
(1)在JavaScript代码的执行过程中,原始数据类型的值和引用类型都是直接保存在栈空间中的,而引用类型指向的值存放在堆空间中
(2)通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。
(3)原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。所以d=c的操作就是把 c 的引用地址赋值给 d,你可以参考下
当内部函数引用了外部函数变量时,JavaScript引擎会将其判断为一个闭包,并将该闭包内所引用到的外部函数变量存放在堆空间中的close对象中。
当JavaScript中的函数执行时,会产生对应函数的执行上下文,并且该执行上下文会被压入到执行栈中,同时在执行栈中还会有一个ESP(记录当前执行状态)的指针,指向调用栈中的当前函数执行上下文。
在当前函数执行完成之后,执行栈将会继续执行下一个上下文,同时ESP指针也会相应下移,此时就相当于完成了一个内存回收的操作。(当有新的执行上下文加入到执行栈中时,原来失效的执行上下文的内容会被覆盖掉)
所以,当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。
(1)代际假说
(2)新生区和老生区
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
对于新生代区域和老生代区域,V8引擎分别用了两个不同的垃圾回收器:
(3)垃圾回收器的工作原理
(4)副垃圾回收器
副垃圾回收器主要负责新生区的垃圾回收。新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。
新加入的对象都会加入对象区域,当对象区域快被写满的时候,就需要进行一次垃圾回收操作。
在垃圾回收的过程中,首先对对象区域中要回收的对象做标记,标记完成之后,正式进入垃圾清理阶段。首先,副垃圾回收器会将存活对象复制到空闲区域,同时将对象有序排列起来,相当于完成了一次内存整理操作。
在完成复制之后,对象区域和空闲区域进行角色翻转。这样就完成了垃圾对象回收的操作。同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
ps.因为复制操作需要时间成本,因此为了执行效率,一般新生区的空间会被设置的很小。同时,为了防止存活对象充满整个区域,JavaScript引擎采用了对象晋升策略——经过两次垃圾回收依然还存活的对象,会被移动到老生区中
(5)主垃圾回收器
主垃圾回收器主要采用标记-清楚的算法进行垃圾回收。
在标记阶段,从一组根元素开始,递归遍历这组元素,在这个遍历过程中,能到达的元素称为活动对象,不能到达的元素就可以判断为垃圾数据。
接下来就是垃圾清除的过程,主垃圾回收器会将在标记过程中被标记为垃圾数据的内容清楚掉,但是这种方式很容易产生大量不连续的内存碎片,因此又产生了标记-整理算法,标记过程和之前保持一致,但是整理过程变成了将所有存活对象往一端移动,然后直接清理掉边界以外的内存。
因为JavaScript是运行在浏览器主线程上的,一旦执行垃圾回收算分发,都需要将正在执行的JavaScript脚本暂停下来,待垃圾回收完毕之后再恢复脚本执行。我们将这一行为称为全停顿。
为了降低老生代的垃圾回收造成的卡顿,v8引擎将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JavaScript应用逻辑交替进行,直到标记阶段完成。我们将这个算法称为增量标记。
(1)编译器:编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了
(2)解释器:由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。
(1)生成抽象语法树(AST)和执行上下文:将源代码转换为抽象语法树,并生成执行上下文。
AST的生成过程:词法分析 -> 语法分析
ps.在Babel和ESLint中都有使用到AST去完成对应操作
(2)生成字节码:字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
ps.机器码所占用的空间远远超过了字节码,所以使用字节码可以减少系统的内存使用。
(3)执行代码
通常,如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
(1)提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,这样可以使得页面快速响应交互;
(2)避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程;
(3)减少 JavaScript 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。