13

《图解 Google V8》专栏总结

 3 years ago
source link: https://zhuanlan.zhihu.com/p/139428079
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

《图解 Google V8》专栏总结

这是一门短小精悍的课程,整个专栏就 24 讲,没有一行一行的分析 Chrome V8 代码。高屋建瓴地拆解 V8 的每一块内容,那估计 100 讲也说不完,编程领域论代码复杂性,老大是操作系统,其次是浏览器,所以专栏作者主要说了 V8 的核心特性,以及从前端角度触发,将两者相结合讲解,对于入门 V8 和进阶高阶 JS 是没什么问题。

v2-27498f4cbd2642f2d6eab82dfad382d6_720w.jpg
来自《图解 Google V8》

从五个纬度拆解 V8 的知识

  • V8 基础环境
  • V8 执行流程
  • 事件循环系统
  • 垃圾回收机制
  • JS 设计思想

V8 基础环境

  • 全局执行上下文
  • 事件循环系统

V8 执行流程

事件循环系统

垃圾回收机制

  • 垃圾回收器,主垃圾回收器和副垃圾回收器
  • 垃圾回收算法,Scavenge 算法(副),标记-清除,标记-整理(主)。
  • 垃圾回收流程

JS 设计思想

  • 函数是一等公民
  • 类型系统和自动垃圾回收
  • 原型链继承

V8 是执行一段 JS 代码,其主核心流程是先将 JS 代码转为低级中间代码或者机器码,再执行机器代码。你可以把 V8 看成是一个虚构出来的计算机,也称为虚拟机,虚拟机通过模拟实际计算机的各种功能来实现代码的执行,如模拟实际计算机的 CPU、堆栈、寄存器等,虚拟机还具有它自己的一套指令系统。这样我们不需要操作系统,就构建自己的应用。

JS 设计思想

  • 函数即对象
  • 快属性和慢属性:V8 如何提升对象属性访问速度
  • 函数表达式
  • 原型链,V8 实现对象继承
  • 作用域链,V8 查找变量
  • 类型转换,实现 1 + “2”
  • 查看 V8 编译工具 D8

V8编译流水线

  • 运行时环境
  • 机器代码,二进制代码被CPU执行
  • 堆和栈,函数调用影响内存布局
  • 延迟解析,实现闭包
  • 字节码,引入中间字节码,体积小,缓存命中率高
  • 隐藏类,可快速查找对象属性
  • 内联缓存,提供函数执行效率

事件循环和垃圾回收

  • 消息队列,实现函数回调
  • 异步编程,实现微任务和宏任务
  • 垃圾回收,主垃圾回收器和副垃圾回收器,Scavenge 算法(副),标记-清除,标记-整理(主)

Chrome V8对于很多前端同学都是黑盒,如果我们要理解每一个设计意图和设计API,无疑是自找没趣,如果能从宏观的角度,先知道V8的大体架构,以及它的演进历程,那么就会明白它的设计意图,也能结合前端的变革一同分析,这无疑是渐进式地,且增加了关联性,比较容易形成自己的理解。所以我强烈推荐这个专栏作为V8的入门和JS进阶。

下面是一些笔记

1、为什么父类上被继承的方法或者属性要写在原型(prototype)上,而不直接绑定到this上?

因为绑定到this上,每次new调用构造函数的时候,都会生成一份新的数据,而prototype是内存共享的。

2、v8有哪些优化手段

我们知道 JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存。

3、V8隐藏类有什么作用?

静态语言,比如C++,在创建对象的时候知道对象属性的类型,即知道对象占用的空间是多少,所以也知道对象的偏移量,通过偏移量可以快速查找属性,和数组下标查找元素,时间复杂度只要O(1)是同一个道理。而V8引入隐藏类,在编译代码的时候,预解析对象的类型,并为对象设置隐藏类。但JS因为运行时可以修改对象的属性,如果动态修改了对象属性类型,很可能导致隐藏类的优化失效,我们在运行时尽量不要修改对象类型,新增或者删除属性也会使隐藏类失效。

4、V8内联缓存

其实 内联缓存(IC,inline cache) 的原理很简单,直观地理解,就是在 V8 执行函数的过程中,会观察函数中一些调用点 (CallSite) 上的关键的中间数据,然后将这些数据缓存起来,当下次再次执行该函数的时候,V8 就可以直接利用这些中间数据,节省了再次获取这些数据的过程,因此 V8 利用 IC,可以有效提升一些重复代码的执行效率。

5、宏任务和微任务

宏任务是在主线程的消息队列里,一个宏任务下面可以有多个微任务。主线程有一个循环,从消息队列中读取宏任务,执行结束再执行下一个。而微任务是在当前执行的调用栈的微任务队列里,微任务其实是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。下面是一张主线程普通任务,宏任务,微任务的示意图。

图片来自《图解 Google V8》专栏

6、async和await

用来解决回调地狱。在之前用generator解决回调地狱,但是状态还需要外部手动控制,所以有心智负担。generator是JS中协程的概念,协程是比线程更轻量的存在,是在内存间执行,无像线程间切换的开销。而async像写同步代码一样写异步流程,支持try catch,接收一个普通函数或者一个promise。

7、V8垃圾回收

分为主垃圾回收和副垃圾回收。分别负责老生代和新生代垃圾回收。

副垃圾回收即频繁触发的垃圾回收,因为回收频繁,所以占用的内存空间少,一般是几M。新生代使用的是scanvenger算法,即将内存空间均分为两个空间,每次将少量数据存入一个空间,当空间满了,就进行一次GC操作,清理没有引用的对象,并且将不连续的内存变成连续的,两个空间互换,并清除最新的。如果一个对象在两次变换中还存在,即晋升到老生代。

老生代是保存大对象,以及存活时间久的对象。使用标记清除算法。先标记,然后清除,但是内存空间里的对象还是不连续,所以就引入了标记-整理(mark-compact)。标记即标记清除,整理即整理内存碎片。

主垃圾回收器同时采用了这三种策略:

首先主垃圾回收器主要使用并发标记,我们可以看到,在主线程执行 JavaScript,辅助线程就开始执行标记操作了,所以说标记是在辅助线程中完成的。

标记完成之后,再执行并行清理操作。主线程在执行清理操作时,多个辅助线程也在执行清理操作。

另外,主垃圾回收器还采用了增量标记的方式,清理的任务会穿插在各种 JavaScript 任务之间执行。

8、V8内存泄漏

闭包可能会导致内存泄漏。所以闭包内引用的对象要及时销毁或者引用少量数据。

9、V8内存回收

如果频繁使用大的临时变量,那么就会导致频繁垃圾回收,频繁的垃圾回收操作会让你感觉到页面卡顿,要解决这个问题,我们可以考虑将这些临时变量设置为全局变量。


最后放一张结业证书,我觉得这个还挺有仪式感的。




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK