今日总结 – 浏览器原理学习笔记03—V8工作原理

浏览器原理学习笔记03—V8工作原理

Write By CS逍遥剑仙
我的主页: www.csxiaoyao.com
GitHub: github.com/csxiaoyaojianxian
Email: [email protected]

1. JavaScript 的数据类型

JavaScript 是弱类型(支持隐式类型转换),动态(运行时类型推断)语言。

JavaScript 的数据类型有 8 种:7 种 原始类型引用类型 (对象)

2. JavaScript 内存空间

2.1 内存模型

function foo(){
    var a = "1"
    var b = a
    var c = {name:"1"}
    var d = c
}
foo()

栈空间通常不会设置太大,存放原始类型的小数据;堆空间很大,存放引用类型的数据,分配和回收内存会占用一定时间。

2.2 闭包对象产生过程

function foo() {
    var myName = "1"
    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("test")
bar.getName()

  1. foo 函数执行并创建执行上下文;
  2. 预扫描 内部函数,编译过程中遇到内部函数如 setNamegetName 则对内部函数进行 快速词法扫描,发现引用了外部函数变量如 myNametest1 则判断为闭包,在堆空间创建 closure(foo) 对象(内部对象 JavaScript 无法访问)来存储闭包变量如 myNametest1
  3. 未被内部函数引用的变量如 test2 仍旧保存在调用栈中;
  4. 当 foo 函数退出,clourse(foo) 依然被其内部的 getNamesetName 方法引用。所以在下次调用 bar.setNamebar.getName 时,创建的执行上下文中就包含了 clourse(foo)

3. 自动垃圾回收

3.1 调用栈中的数据回收

JavaScript 引擎通过向下移动 ESP (记录当前执行状态的指针) 来销毁函数保存在栈中的执行上下文,效率很高。

function foo(){
    var a = 1
    var b = {name:"test1"}
    function showName(){
      var c = "1"
      var d = {name:"test2"}
    }
    showName()
}
foo()

3.2 堆中的数据回收

3.2.1 代际假说和分代收集

代际假说:
大部分对象在内存中存在的时间很短
短期不死的对象,会活得更久

堆中的垃圾数据使用 JavaScript 中的垃圾回收器进行回收。V8 把堆分为 新生代老生代 两个区域,新生代中存放生存时间短的对象,老生代中存放生存时间久的对象。新生区通常只支持 1~8M 的容量,使用 副垃圾回收器 进行回收。老生区支持的容量较大,使用 主垃圾回收器 进行回收。

3.2.2 副垃圾回收器

新生代中用 Scavenge 算法 (空间对半划分为对象区域和空闲区域) 来处理。

新对象存放到对象区域,当对象区域快满时执行一次垃圾清理,先对对象区域中的垃圾做标记,再将存活的对象有序复制到空闲区域中,相当于完成了内存整理操作。完成复制后两个角色翻转,完成了垃圾清理。

因为复制时间不宜过长,一般新生区空间会设置得比较小,也因此很容易填满,JavaScript 引擎采用了 对象晋升策略 来解决,即经过两次垃圾回收依然存活的对象会被移动到老生区中。

3.2.3 主垃圾回收器

老生区中的对象占用空间大、存活时间长,一部分来自新生区中晋升的对象,一部分来自直接分配的大对象。

老生代采用 标记 – 清除 (Mark-Sweep) 算法进行垃圾回收。标记阶段从调用栈根元素开始递归遍历,根据能否到达区分 活动对象垃圾数据

垃圾清除阶段清除垃圾数据并产生大量不连续的内存碎片。再使用 标记 – 整理 (Mark-Compact) 算法在标记后将所有存活的对象移向一端,再直接清理边界以外的内存空间。

3.2.4 全停顿

由于 JavaScript 运行在渲染进程主线程上,执行垃圾回收将导致暂停执行 JavaScript 脚本,即 全停顿 (Stop-The-World)。为减少全停顿,V8 将标记过程分为多个的子标记过程,与 JavaScript 应用逻辑交替进行,直到标记阶段完成,称作 增量标记 (Incremental Marking) 算法。

4. 编译器和解释器

4.1 V8 执行 JavaScript 代码总览

编译器和解释器的区别。

JavaScript 是解释型语言,V8 执行 JavaScript 代码的流程总览。

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

4.2.1 AST 的应用

var myName = "sunshine"
function foo(){
  return 18;
}
myName = "csxiaoyao"
foo()

经过 javascript-ast 处理后生成的 AST 结构如下:

AST 是非常重要的一种数据结构,编译器或解释器依赖于 AST,而非源代码。AST 在很多项目中有着广泛的应用,如:

  1. Babel 的工作原理是先将 ES6 源码转换为 AST,再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码
  2. ESLint 将源码转换为 AST,再利用 AST 来检查代码规范化的问题

4.2.2 AST 的生成

生成 AST 需要经历分词和解析两个阶段。

1. 分词 / 词法分析 (tokenize)

将源码拆解成一个个语法上不可再分、最小的单个字符或字符串 token

2. 解析 / 语法分析 (parse)

将 token 根据语法规则转为 AST

4.3 生成字节码

生成 AST 和执行上下文后,解释器 Ignition 会根据 AST 生成字节码,并解释执行字节码。

引入字节码是为了解决起初 V8 直接将 AST 转换为机器码而导致移动设备内存占用高的问题,字节码就是介于 AST 和机器码之间的一种代码,与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

4.4 执行代码 & JIT

第一次执行的字节码,解释器 Ignition 会逐条解释执行。在执行字节码的过程中,如果发现有热点代码(HotSpot),即一段代码被重复执行多次,后台编译器 TurboFan 会把该段热点字节码编译为高效的机器码,提高后续执行效率。字节码配合解释器和编译器的技术称为 即时编译 (JIT)。

4.5 JavaScript 性能优化策略

  1. 提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,使得页面快速响应交互;
  2. 避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程;
  3. 减少 JavaScript 文件的容量,提升下载速度,并且占用更低的内存。
正文完