V8工作原理

01 栈空间和堆空间:数据是如何存储的?

​ JavaScript 是一种弱类型的、动态的语言。这意味着我们不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,且可以使用同一个变量保存不同类型的数据。

img

​ 如图,JavaScript 一共有 8 种数据类型,它们可以分为两大类——原始类型(前7种)和引用类型(最后1种)。其中,原始类型的数据是存放在栈中,引用类型的数据是存放在堆中的。堆中的数据是通过引用和变量关联起来的。也就是说,JavaScript 的变量是没有数据类型的,值才有数据类型,变量可以随时持有任何类型的数据。

​ 为什么一定要分“堆”和“栈”两个存储空间呢?因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据。

​ 从内存模型的角度再来谈谈闭包:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
  • 当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文。

  • 在编译过程中,遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。

  • 接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了 myName 和 test1 两个变量了。

  • 由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中。

    可以画出执行到return innerBar语句时的调用栈状态:

img

​ 从上图可以清晰地看出,当执行到 foo 函数时,闭包就产生了;当 foo 函数执行结束之后,返回的 getName 和 setName 方法都引用“clourse(foo)”对象,所以即使 foo 函数退出了,“clourse(foo)”依然被其内部的 getName 和 setName 方法引用。

02 垃圾回收:垃圾数据是如何自动回收的?

​ 垃圾数据回收分为手动回收(C/C++,由代码控制)和自动回收(JavaScript,Java等,由垃圾回收器来释放)两种策略。

2.1 调用栈中的技术是如何回收的
function foo(){
    var a = 1
    var b = {name:"极客邦"}
    function showName(){
      var c = 2
      var d = {name:"极客时间"}
    }
    showName()
}
foo()

​ 当一个函数执行结束之后,JavaScript 引擎通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。如图为从栈中回收 showName 执行上下文:

img

​ 从图中可以看出,当 showName 函数执行结束之后,ESP 向下移动到 foo 函数的执行上下文中,上面 showName 的执行上下文虽然保存在栈内存中,但是已经是无效内存了。比如当 foo 函数再次调用另外一个函数时,这块内容会被直接覆盖掉,用来存放另外一个函数的执行上下文。

2.2 堆中的数据是如何回收的

​ 在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。副垃圾回收器,主要负责新生代的垃圾回收。主垃圾回收器,主要负责老生代的垃圾回收。不论什么类型的垃圾回收器,它们都有一套共同的执行流程:标记空间中活动对象和非活动对象->回收非活动对象所占据的内存->内存整理(频繁回收对象后,内存中会存在大量不连续空间,即内存碎片)

2.2.1 新生代

算法:Scavenge 算法
原理:
1、把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。
2、新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
3、先对对象区域中的垃圾做标记,标记完成之后,把这些存活的对象复制到空闲区域中
4、完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。

对象晋升策略:经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

2.2.2 老生代

(1)算法:标记 - 清除(Mark-Sweep)算法
原理:
1、标记:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象(比如某块数据被一个变量b引用了,那么这块数据会被标记为活动对象),没有到达的元素就可以判断为垃圾数据。
2、清除:将垃圾数据进行清除。

碎片:对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。

(2)算法:标记 - 整理(Mark-Compact)算法

​ 标记过程仍然与标记 - 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

(3)优化算法:增量标记(Incremental Marking)算法
原理:
1、为了降低老生代的垃圾回收而造成的卡顿
2、V8把一个完整的垃圾回收任务拆分为很多小的任务
3、让垃圾回收标记和 JavaScript 应用逻辑交替进行

03 编译器和解释器:V8是如何执行一段JavaScript代码的?
3.1 编译器和解释器

img

3.2 V8 执行一段代码的流程

img

(1)生成抽象语法树(AST)和执行上下文

​ 执行上下文主要是代码在执行过程中的环境信息,而AST可以看成是代码结构化的表示。AST的生成过程,先分词(词法分析),再解析(语法分析)。

​ 补充:AST是一种非常重要的数据结构,有着广泛的应用。如Babel (可以将 ES6 代码转为 ES5 代码)的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码;ESLint (检查 JavaScript 编写规范的插件)的检测流程也是需要将源码转换为 AST,然后再利用 AST 来检查代码规范化的问题。

(2)生成字节码(解释器)

​ 字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

img

(3)执行代码

​ 解释器 Ignition 除了负责生成字节码之外,它还有另外一个作用,就是解释执行字节码。通常,如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。

​ 补充:Ignition->点火器,TurboFan->螺旋增压,寓意着代码启动时通过点火器慢慢发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高。

3.3 即时编译技术JIT

img

3.4 JavaScript性能优化

​ 对于优化 JavaScript 执行效率,应该将优化的中心聚焦在单次脚本的执行时间和脚本的网络下载上,主要关注以下三点内容:

(1)提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,这样可以使得页面快速响应交互;

(2)避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程;

(3)减少 JavaScript 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值