本质是一个虚拟机,将代码解释为二进制指令
- https://mp.weixin.qq.com/s/pv_4YRo6KjLiVxLViZTr2Q
- https://mp.weixin.qq.com/s/hARJrq_baizVkW5SPUl81Q
- v8作者blog
- v8优化骚套路
- 对象模式
- v8全文档
- chromium下载地址
- 深入了解v8
- 三色标记
- 白色:收集器还未发现该对象
- 灰色:收集器发现,并已推到标记工作表
- 黑色:对象从标记工作表弹出,并已访问其全部字段
- 当没有灰色对象时,标记结束。所有剩余的白色对象都可以安全地被回收。
在 V8 引擎的 5.9 版本之前,有两个编译器:
- full-codegen —— 一个简单快速的编译器,可以生成简单但是相对比较慢的机器码。
- Crankshaft —— 一个更加复杂的(即时)优化编译器,可以产生高度优化的代码。
- Ignition 译码器
- 较小的性能抖动
- 提高启动速度
- 改进基线性能
- 减少内存使用
- 支持新的语言特性
- 顺序:js -> 字节码 -> 机器码
- js编译后的字节码,会占用15%的v8堆内存
- 每次循环会做in判断导致比for-loop慢
- --print-bytecode // 打印字节码
- --trace-gc // 查看内存
- --trace_gc_verbose // 查看内存明细
- --allow-natives-syntax // 允许使用V8 引擎内部调试函数
// clone depot_tools
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
// set path
export PATH=$PATH:/path/to/depot_tools
// restart terminal
// use `fetch` in depot_tools to get v8 source
fetch v8
// stay up to date
git pull origin
gclient sync
// get gn library
gclient runhooks
// add `gm` to alias
alias gm=/path/to/v8/tools/dev/gm.py
// build V8 for known configurations
gm x64.release
// run specific tests
gm x64.debug mjsunit/string-split
// build & run all test
gm x64.release.check
// run benchmarks
gm x64.debug benchmarks- V8(Chrome和NodeJS)
- SpiderMonkey(FireFox)
- Chakra(IE和Eage)
- JavaScriptCore(Safari/ReactNative)
- 解析器解析成ast
- 解释器解析成字节码(效率高,执行慢)
- 编译器将频繁调用的字节码编译替换成机器码(效率低,执行快)
- 如果机器码推测是错误的,会执行“去优化”,还原成字节码
这块可以参考shape&InlineCaches
V8 编译后的代码 Chrome 有两级缓存
- Isolate缓存
- 尽可能使用已存在的数据(命中率80%)
- 编译后,以源码为key存在hashtable(v8堆)中
- 编译其他脚本时检测hashtable,是否存在可复用
- 完整序列化的硬盘缓存
-
不改资源url?
-
明确代码执行行为路径(一些random操作对缓存查找路径会有影响)
-
不变&常变代码分离(不变代码长期缓存,也能用于其他tab页面)
-
IIFE 启发式
// 编译器扫到function 关键字之前的 (就会做编译 // 不过除非必要,否则不推荐用 const bar = (function() { // Eagerly compiled });
-
合并小文件
- 1kb以下文件不会被缓存
debug for v8
下载一个编译好的d8
- 解压,sudo ./d8
- 进入命令模式,直接可以输入js
./d8 ./test.jsParser
js转ast
ignition
即解释器,将ast转二进制码(bytecode)并执行,同时收集TurboFan优化编译所需信息。
通常有两种类型解释器:
- 基于寄存器(现在的v8),优势:
- 更快(相较栈,可以用更少的指令,或者说更少的内存访问完成操作;内存访问是执行速度的一个瓶颈)
- 基于栈(java、.Net、早期v8),优势:
- 实现简单
- 节省资源
- 可移植性
TurboFan
即编译器,将二进制码(bytecode)转为汇编
Orinoco
即垃圾回收器
在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量,
所以在javascript,函数是一等公民
静态作用域:编译时即可确定上下引用关系
动态作用域:执行时确认引用关系
解析器在解析过程中,会跳过函数内部代码,而只生产顶层代码的ast和字节码,原因在于:
- 减少用户等待时间
- 降低内存占用
闭包的问题就是游离态的变量,不会随执行上下文被销毁
v8引入了预解析器,即遇到一个函数,不会跳过,而是做一次快速检查:
- 函数是否有语法错误,有则直接抛错
- 函数是否引用了外部变量,有则会将栈中的变量复制到堆中,下次执行函数时,直接使用堆中的引用,以此解决闭包问题(内存泄漏)
// 举例:
function Foo() {
this[200] = 'test-200';
this[1] = 'test-1';
this[100] = 'test-100';
this['B'] = 'bar-B'
this[50] = 'test-50';
this[9] = 'test-9';
this[8] = 'test-8';
this[3] = 'test-3';
this[5] = 'test-5';
this['D'] = 'bar-D';
this['C'] = 'bar-C';
}
var bar = new Foo();
for (key in bar) {
console.log(`index:${key} value:${bar[key]}`);
}
// 请问key输出顺序是什么?
// 在ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。v8默认会使用线性结构存储,即排序属性和常规属性:
-
数字属性称为elements,排序属性
-
字符串称为properties,常规属性,其中部分(默认10个)常规属性会直接存储到对象本身,称为对象内属性
当对象中属性过多,或者存在反复添加/删除属性的操作,v8会将线性存储降为非线性的字典存储模式(降低查找速度,提升属性修改速度)

