Description
V8 是 Chrome 浏览器的 JS 引擎,浏览器是客户端,客户端在解析执行 JS 代码时,就需要有相应的引擎来做这项工作,V8 就是一个强大的引擎。
v8 的内存模型
我们平时可能听到过,JS 有基本数据类型和引用类型,基本数据类型是存放在栈里,而引用类型是存放在堆里。这里面的「栈」和「堆」指的就是 v8 引擎里的内存模型。v8 将内存空间分为代码空间
,栈空间
和 堆空间
。所谓代码空间就是指可执行代码,栈空间就是我们常看到的调用栈,调用栈里放的是执行上下文。而堆空间里存放引用类型,每个数据都会分配内存,拿到内存地址,然后这个地址再被引用。比如执行上下文里变量环境里的变量数据,如果是基本数据类型,则是值;如果是引用类型,那就是这个内存地址。
那 v8 为什么要这么划分两种数据类型的存放呢,全部放栈空间里不行吗?答案是不行,因为 JS 引擎需要用栈来维护程序执行期间上下文的状态,如果你栈数据太大了的话,那么势必会影响到上下文切换的效率,进而又影响到整个程序的执行效率。所以通常情况下,栈空间都不会很大,而且基本数据类型占用的空间一般也很少。那 v8 是怎么对内存里的数据进行管理的呢?这就要说说垃圾回收机制。
垃圾回收机制
参考前文 v8 下的垃圾回收机制
v8 是如何执行一段 JS 代码的
这里面的执行机制很复杂,首先要了解几个基础概念:编译器
,解释器
,字节码
,抽象语法树
。
我们平时写的源代码,计算机是看不懂的,所以编译器做的工作就是转译我们的代码,转成什么呢?从人类理解的高级语言转成计算机能理解的机器语言。编译器的工作流程大致是:
- 通过词法分析,语法分析将源代码转换为 AST 抽象语法树
- 然后进行词义分析,生成中间代码
- 接着进行代码优化,将中间代码转为二进制文件
- 最后变成可执行代码
而说到解释器,众所周知,JS 是一门解释型语言,相对应的另一个概念叫编译型语言,比如 Java,C/C++。解释器就是负责解释执行经过处理的 JS 代码,注意,这里所谓的处理,就是代码已经经由各种转换操作,变成机器能理解的语言了。解释器的工作流程是:
- 通过词法分析,语法分析将源代码转换为 AST
- 进行词义分析,生成字节码
- 最后根据字节码来解释,执行代码
接下来再说说抽象语法树 AST,顾名思义,这是一种树形结构,目的是让解释器和编译器能看懂并进行各种分析工作。可以类比的还有 HTML 代码转成 DOM 树,也是为了方便计算机理解并执行操作。再比如我们所熟知的 Babel 在转换 ES6 新特性时,也是先将代码转为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码。总的来说,AST 是一颗树,把一段 JS 代码分析拆分成一个个小的单元组织起来。
然后再说说字节码。字节码的出现是为了解决内存占用的问题,在没有字节码的时候,v8 在生成 AST 后,下一步就是直接生成机器码了,机器码因为是底层语言,执行效率非常高,效率高的代价就是占内存,v8 在转换成机器码的过程中需要大量的内存使用。而随着 Chrome 在手机上的普及,问题就出现了,比如一台只有 512M 内存的手机,直接转换机器码的过程,内存可能就飙升进而带来使用问题。所以 v8 团队重构了引擎架构,引入了字节码。所谓字节码就是介于 AST 和机器码的一种代码,需要通过解释器将其转换为机器码才能执行。可以这么理解,字节码就是在 AST 到机器码中间取一个平衡,它所占用的内存比机器码小,所以先由 AST 过渡到字节码,再通过 JIT 技术变为机器码。
理解了编译器,解释器, AST 还有字节码的概念后,接下来就可以说说 v8 是如何执行一段 JS 代码了。
-
首先,v8 会对源代码进行转换,转换的结果是生成 AST 抽象语法树还有我们熟悉的执行上下文。
AST 的生成过程需要经过两个阶段:
第一个阶段是分词(tokenize),将源代码拆分成一个个不可再分的 token,比如关键字,标识符,运算符,字符串;
第二个阶段是解析(parse),将上一步生成的 token 根据语法规则生成 AST。这一步如果存在语法错误,就会抛出语法错误的提示。
至于执行上下文,可参考前文 JS 中的执行上下文 -
有了 AST 后,下一步就要将 AST 转换成字节码。这一阶段的主角就是解释器 Ignition(点火装置) ,解释器会逐步转换 AST ,并生成字节码
-
有了字节码后,接下来就进入执行阶段了。执行阶段的主角是解释器 Ignition + 编译器 TurboFan。 通常,如果是第一次执行的字节码,解释器就会逐条解释执行,在这个过程中,如果发现热点代码(也就是重复出现的代码),那么后台的编译器 TurboFan 就会将这段热点代码编译为机器码,这就是代码优化。这样当下次再执行这段被优化过的代码时,就能直接使用高效的机器码,从而大大提升执行效率。那如果不是热点代码呢?那就只能由解释器执行后,再生成机器码了。
-
这种字节码和解释器编译器相互配合执行的过程,就是所谓的
JIT(即时编译)
。
理解 JS 代码的执行过程对我们有什么好处呢?除了知晓原理外,还有一个关键:性能优化。
性能优化
关于性能优化,网上有很多文章学习。这里提两点跟 v8 执行有关的:
- JS 是运行在主线程的,那么我们写代码的时候要注意,避免大的耗时任务长时间占用主线程,造成无响应的卡死状态。
- JS 内联脚本不能太大,因为解析 HTML 的过程中,如果遇到脚本,就会去执行脚本,解析和编译的过程也是占用主线程的。当然有很多方法可以避免,比如给脚本设置 async,defer 属性等。
总结
理解 v8,首先要理解它的内存模型,知道我们平时 JS 里的基本数据类型和引用类型是存放在哪的;然后就是对应的内存是怎么被释放的,也就是垃圾回收的过程;最后就是理解几个重要概念 编译器
,解释器
,字节码
,抽象语法树
,即时编译(JIT)
,然后才能深入了解 v8 执行 JS 代码的过程。