基本信息

填写书籍的基本信息

  • 作者:暮桥
  • 类别:技术分享
  • 简介:V8 引擎绝对是其中的佼佼者,chrome 和 node 底层都使用了 V8 引擎,其中 chrome 的市场占有率已经达到 70%,而 node 更是前端工程化以及扩展边界的核心支柱,V8 引擎对于一个前端开发工程师来说重要程度可想而知。
  • 推荐指数:⭐️⭐️⭐️⭐️
  • 阅读周期

    • 开始日期:2021.11.12
    • 终止日期:2021.11.12

书摘

鼠标放置在正文左侧,点击 “+” 工具栏中的“高亮块”,高亮你的读书感悟。

第 1 章 概述

第 2 章 抽象语法树 AST

第 3 章 字节码

  • 参考文献:https://juejin.cn/post/6844904152745639949
  • 什么是字节码
  • 早期 V8:直接编译为二进制文件,并且进行缓存二进制代码机制
    • 内存占用大
    • 代码复杂性高,可迁移性差
    • 惰性编译带来的缓存问题
  • 现代 V8
    • js 转化为 AST
    • Ignition 解释器将 AST 编译为字节码
    • Turbofan 引擎标记热点代码,编译成为更高效率的二进制代码

【评论 & 感言】

  • 整体的思想和 WebAssembly 很相似啊!!!

第 4 章 字节码的执行流程

  • 参考文献:https://juejin.cn/post/6844904161163608078
  • 字节码的执行流程
    • Ignition 解释器:类似 JVM,本质是虚拟机。
      • 基于栈的虚拟机:JVM
      • 基于寄存器的虚拟机:Ignition【更快,但指令更长】【创建一个虚拟空间来保存参数,计算中间结果】
  • 感想:面试被问的一道问题 switch 为什么比 if else 快,当时回家查了一下相关文章发现有人说 switch 跳表巴拉巴拉的,我专门转成了字节码看了一下,发现起码在正常的 v8 环境中 switch 比 if else 快纯属扯淡,从字节码上发现,绝大部分条件下两个几乎都是一样快,如果有兴趣可以自己玩一下。

第 5 章 内联缓存

  • 参考文献:https://juejin.cn/post/6844904167333429256
  • 内联缓存 Inline Cache
    • 原理:在运行过程中,收集一些数据信息,将这部分信息缓存起来然后在再次执行的时候可以直接利用这些信息,有效的节省了再次获取这些信息的消耗,从而提高性能。
    • 本质上就是标记一些调用点,然后为他们分配一个插槽缓存起来,当再次调用的时候直接通过缓存的内存地址获取值。
    • V8 的重要优化策略
  • 内联缓存的单态和多态
    • 传递参数的数据结构不固定的,需要多态内联缓存
    • 多态内联缓存的原理:在同一个 slot 位置上,不止缓存一份数据。【在循环执行的过程中,第二次如果信息和第一次相同,则直接调用;如果不同,则同一个 slot 增加一个新的信息】
    • 多态内联缓存的执行效率会变慢。

第 6 章 内存结构

  • 本身 javascript 只是一种语言,真正进行内存调用分配的是 javascript 依赖的引擎
    • 后进先出,栈空间连续,分配空间和销毁空间需要移动下指针,适合管理函数调用
    • 因为空间连续,空间有限,不方便存放大数据
  • 基础类型:undefined, null, Number, String, Boolean, Symbol【存储在栈中】
    • 基础类型的值在创建时会开辟一块内存空间,将内存地址存储在对应的变量上,如果此时再创建一个基础类型等同于之前创建过的值,会直接将地址存储在新创建的变量上
    • a === d
  • 引用类型:Object、Array、Function【存储在堆中】
    • 那么如果创建一个对象,就会在堆中开辟一块空间用来存储对象,将内存地址存储在对应的变量上,如果此时创建一个新的变量(f)赋值为之前所创建的存储对象地址的变量(c)
    • c === f
  • 堆空间结构
    • 新生代内存区:每个 semispace 默认 16MB,一个是 from space 和 to space
    • 老生代内存区:保存比较持久的对象,一个是 old pointer space(存活的指针信息),另一个是 old data space(存货的数据信息)
    • 大对象区:体积超越其他区大小的对象,主要为了避免大对象的拷贝,使用该空间专门存储大对象。
    • 单元区、属性单元区、Map 区:Map 空间最大限制 8Mb,每个 Map 对象固定大小,为了快速定位,所以空间独立
    • 代码区:存放代码对象,最大限制是 512Mb,唯一有执行权限的内存
  • 内存运行的生命周期
    • 创建对象 obj
    • obj 分配到新生代内存区的其中一个 space,假设为 from 区域
    • 如果 from 区域达到上限 16Mb,V8 的垃圾回收机制会清理 from 区域的不再使用对象(实际上只是先做标记,拷贝的时候清理)
    • 清理后如果还有存货对象,会被复制到 to space,删除所有 from space 对象
    • 之后新创建的对象会被分配到 to space,满了再向 from space 区域
    • 持续一段时间
    • 如果 obj 依然存活在新生代内存区,会被转移到老生代内存区
    • 持续一段时间
    • obj 不被引用,V8 会在老生代内存区遍历,并标记 obj
    • 分批回收待清理对象【单线程执行机制】

第 7 章 垃圾回收机制

  • 垃圾回收的概念:V8 引擎执行代码遇到函数,需要给函数创建上下文并添加到调用栈顶部,函数执行过程中需要分配内存去创建局部变量、参数等等,当函数执行完毕,作用域销毁,变量也要销毁。销毁变量回收内存的过程就是垃圾回收。
  • JavaScript 语言的特点:单线程【代码按顺序执行,同一时间只能处理一个任务】。
    • 存在的问题:V8 垃圾回收过程会阻塞其他的任务。
  • 垃圾回收器
    • 代际假说(The Generational Hypothesis):
      • 大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问。
      • 不死的对象,会活的更久。
    • 根据代际假说设计新生代和老生代内存区,同时设计两个垃圾回收器
      • 副垃圾回收器:新生代内存区的垃圾回收
      • 主垃圾回收器:老生代内存区的垃圾回收
  • 副垃圾回收器
    • 新生代和老生代内存区必然状态:to space 空闲,from space 工作
    • 当 from space 即将到达上限,V8 引擎会在 from space 进行垃圾清理操作,对该区域不再使用的对象进行标记;当复制到 to space 的时候,会将未被标记的对象进行复制。
  • 晋升机制
    • 条件【同时满足】:
        1. 经过一次 Scavenging 算法,且未被标记删除;
        1. 翻转置换的时候,被复制的对象大于 to space 空间的 25%。
    • 晋升后对象会被分配到老生代内存区
  • 主垃圾回收器
    • 标记-清除法
      • 标记阶段:从一组根元素开始,递归遍历根元素,能到达的元素是活动对象,没有到达的是垃圾对象
      • 清理阶段:直接把标记垃圾的数据清理掉
      • 问题:会产生大量不连续的内存碎片,需要通过标记-整理法进行回收
    • 标记-整理法
      • 标记阶段
      • 整理阶段:将未标记的对象(存活对象)进行左移,移动完成后清理边界外的内存
  • 垃圾回收优化策略(Orinoco+V8 优化的空闲回收)
    • 评价垃圾回收的标准:执行垃圾回收的时候,主线程挂起的时间。
    • 并行垃圾回收
      • 背景:新生代内存区【标记-> 复制-> 清理】和老生代内存区【标记-> 清理-> 紧凑】没有任何依赖关系,可以并行执行。
  • 增量垃圾回收
  • 背景:一个大对象的标记需要很长的时间
  • 方法:将一个大的任务分解为小块,允许应用程序在块之间运行
  • 技术方案:使用标记位和标记工作表进行标记
    • 标记位为 00【白色】:未被根节点应用的对象
    • 标记位为 10【灰色】:一个对象被应用会被标记为灰色,并加入标记工作表,等待遍历自身和子对象。
    • 标记位为 11【黑色】:标记为灰色的节点经过遍历所有的子对象之后,结束之后该父节点会标记黑色
    • 如果 标记工作表 中 没有了灰色 的对象,那么代表所有的对象都是 黑色 或者 白色,之后可以放心的清理掉 白色 的对象。
  • 问题:如果标记好的数据被主线程修改,使用写屏障机制,强制让黑色的对象不能直接指向白色对象,将新写入的对象从白色标记为灰色,标记工作表就不会空。
  • 并发垃圾回收
    • 和并行垃圾回收的区别:并行发生在主线程和工作线程上,整个应用程序在并行垃圾回收阶段暂停;并发则发生在工作线程上,并发垃圾回收进行时,应用程序可以正常运行
  • 空闲时垃圾回收
    • 对任务队列占用率进行监控,估计 V8 何时进入空闲状态以及持续的时间,一些优先级不高的垃圾回收任务会在此空闲时间做。
    • Chrome 的 Task scheduler 可以进行估算。

第 8 章 消息队列

  • 为什么 JavaScript 是单线程
    • 最初设计是用来交互、操作 DOM 的,如果有多个线程会比较难处理。
    • HTML5 提出了 web worker 概念,允许额外开启线程,但 worker 线程完全受到主线程控制
    • JS 同一时间只能执行一个任务,任务会形成任务队列。
    • 优化异步任务执行的方法:消息队列事件循环
  • 消息队列
    • 先进先出;主要用来存放需要执行的任务。
    • 场景 1:onClick 等异步交互,由 DOM Binding 处理,事件触发时,回调函数被添加到消息队列中。
    • 场景 2:ajax 请求异步由 network 处理,网络请求返回后的回调函数会添加到消息队列
    • 场景 3:Timer 事件到达会将回调函数添加到任务队列
  • 消息队列的执行:
    • 每次执行栈中的代码就是一个宏任务(task),而消息队列中的任务会按顺序放到下一次的宏任务(task)中,每个宏任务(task)在执行时,V8 都会重新创建栈,然后随着宏任务(task)中函数调用,栈也随之变化,最终,当该宏任务(task)执行结束时,整个栈又会被清空,接着主线程继续执行下一个宏任务(task)
    • 一个宏任务 task 完成后,下一个宏任务 task 开始前的页面重新渲染:
  • 宏任务需要更加细粒度,引入 promise 机制,增加微任务:
  • 事件循环
    • 主线程运行产生执行栈,调用执行栈过程中调用一些异步函数
    • 满足异步函数的触发条件时,将对应的回调函数推送到消息队列中
    • 当主栈的代码执行完毕,会执行页面渲染,然后创建新的主栈
    • 将消息队列中的回调函数推送到主栈,然后顺序执行主栈的任务

第 9 章 协程和生成器函数

  • 生成器函数 Generator:ES6 引入(Generator+Promise 解决回调地狱) -> ES7(async/await 语法糖)
    • JS 最初规则:一个函数开始就会 return,运行期间没有其他代码可以打断它。
    • Generator 函数可以暂时交出函数的执行权
  • Generator 函数特点
    • 调用 Generator 函数会返回一个内部指针(迭代器对象)
    • 调用迭代器对象的 next 方法,用于遍历 Generator 函数内部的每个状态
    • function 关键字需要加上*
    • 函数体内用 yield 定义不同的内部状态,迭代器对象使用 next 可以返回当前的 yield 状态
function* gen() {
    yield 'first';
    yield 'second';
    yield 1 + 2;
    return 'end';
}

let g = gen();
g.next(); // {value: 'first', done: 'false'};
g.next(); // {value: 'second', done: 'false'};
g.next(); // {value: '3', done: 'false'};
g.next(); // {value: 'end', done: 'true'};
  • 协程
    • 比线程更轻量级的存在,一个线程可以存在多个协程,但是只能同时运行一个协程。
    • 本质是在线程的基础上,增加了任务栈的切换,进行线程粒度的代码交替,实现并发。
  • 协程的优点
    • 避免锁竞争:协程本身是单核 CPU 的操作,所以不存在竞争关系
    • 协程消耗资源远比线程小:避免 OOM
    • 切换成本极低

收获 & 感悟

记录全书阅读的心得和体会

  1. 对于《编译原理》、《操作系统》和前端开发的内容更熟悉了;
  2. 解答了当时微信二面的问题。

书评

从多个维度分析、评价书籍

  1. 讲的很容易懂,很多图片让人非常容易接受;
  2. 讲的太浅显了。