关于v8:D8调试工具jsvu的使用细则

d8 is V8’s own developer shell. D8 是一个十分有用的调试工具,你能够把它看成是 debug for V8 的缩写。咱们能够应用 d8 来查看 V8 在执行 JavaScript 过程中的各种两头数据,比方作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还能够应用 d8 提供的公有 API 查看一些外部信息。 前言jsvu 是 JavaScript 引擎版本管理工具 以下是在Windows10下的操作,倡议在 CMD 窗口外面操作。 1、装置前提:node V14+ npm install -g jsvu运行 jsvu,交互式命令行抉择须要装置的平台和引擎 装置指定版本的引擎能够参考上面的命令 jsvu --os=win64 --engines=v8,v8-debug 执行 jsvu装置引擎,可在 %USERPROFILE% /.jsvu 目录下查看装置的引擎 装置 v8-debug jsvu --os=win64 --engines=v8-debug 操作系统反对的引擎JavaScript engineBinary namemac64mac64armwin32win64linux32linux64Chakrachakra or ch✅❌✅✅❌✅GraalJSgraaljs✅❌❌✅❌✅Hermeshermes & hermes-repl✅❌❌✅❌✅JavaScriptCorejavascriptcore or jsc✅✅❌✅ *❌✅QuickJSquickjs❌❌✅✅✅✅SpiderMonkeyspidermonkey or sm✅✅✅✅✅✅V8v8✅✅✅✅✅✅V8 debugv8-debug✅✅✅✅✅✅XSxs✅ (32)❌✅✅ (32)✅✅查看jsvu版本jsvu -h jsvu v1.13.3 — the JavaScript engine Version Updater [<engine>@<version>][--os={mac64,mac64arm,linux32,linux64,win32,win64,default}][--engines={chakra,graaljs,hermes,javascriptcore,quickjs,spidermonkey,v8,v8-debug,xs},…]Complete documentation is online:https://github.com/GoogleChromeLabs/jsvu#readme2、装置 eshost-cli(这个不装置也不影响应用)治理js引擎,能够调用多个引擎执行js代码,更加不便调试不同引擎下的代码 ...

August 17, 2022 · 4 min · jiezi

关于v8:深入理解之V8引擎的垃圾回收机制

本文谨用于笔者集体了解和总结V8引擎的垃圾回收机制,本文次要参考一文搞懂V8引擎的垃圾回收 在理解V8垃圾回收机制之前,咱们先来论述一些概念: 「全进展」:垃圾回收算法在执行前,须要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑。如果一次GC须要50ms,应用逻辑就会暂停50ms。为什么会暂停呢? ①因为js是单线程执行的,进入垃圾回收后,js应用逻辑须要暂停,以留出空间给垃圾回收算法运行。②垃圾回收其实是十分耗时间的操作。 V8引擎垃圾回收策略:V8的垃圾回收策略次要是基于分代式垃圾回收机制,其依据对象的存活工夫将内存的垃圾回收进行不同的分代,而后对不同的分代采纳不同的垃圾回收算法。在新生代的垃圾回收过程中次要采纳了Scavenge算法;在老生代采纳Mark-Sweep(标记革除)和Mark-Compact(标记整顿)算法。在理解新生代和老生代的垃圾治理算法之前,咱们无妨先来理解一下V8引擎垃圾治理的内存构造; V8引擎垃圾治理的内存构造:新生代(new_space):大多数的对象开始都会被调配在这里,这个区域绝对较小然而垃圾回收特地频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将须要保留的对象复制过去。(笔者看到了两种分区说法:①From区:To区=1:1②From区:To区:To区=8:1:1,本文仅用来理解回收机制,不对此处过多探讨)老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,绝对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者蕴含大多数可能存在指向其余对象的指针的对象,后者只保留原始数据对象,这些对象没有指向其余对象的指针。大对象区(large_object_space):寄存体积超过其余区域大小的对象,每个对象都会有本人的内存,垃圾回收不会挪动大对象区。代码区(code_space):代码对象,会被调配在这里,惟一领有执行权限的内存区域。map区(map_space):寄存Cell和Map,每个区域都是寄存雷同大小的元素,构造简略。新生代区:新生代区次要采纳Scavenge算法实现,它将新生代区划分为激活区(new space)又称为From区和未激活区(inactive new space)又称为To区。程序中生命的对象会被存储在From空间中,当新生代进行垃圾回收时,处于From区中的尚存的沉闷对象会复制到To区进行保留,而后对From中的对象进行回收,并将From空间和To空间角色对换,即To空间会变为新的From空间,原来的From空间则变为To空间。因而,该算法是一个就义空间来换取工夫的算法。 基于上述算法,算法图解实现如下(转载): 假如咱们在From空间中调配了三个对象A、B、C当程序主线程工作第一次执行结束后进入垃圾回收时,发现对象A曾经没有其余援用,则示意能够对其进行回收对象B和对象C此时仍旧处于沉闷状态,因而会被复制到To空间中进行保留接下来将From空间中的所有非存活对象全副革除此时From空间中的内存曾经清空,开始和To空间实现一次角色调换当程序主线程在执行第二个工作时,在From空间中调配了一个新对象D工作执行结束后再次进入垃圾回收,发现对象D曾经没有其余援用,示意能够对其进行回收象B和对象C此时仍旧处于沉闷状态,再次被复制到To空间中进行保留再次将From空间中的所有非存活对象全副革除From空间和To空间持续实现一次角色调换对象降职:当一个对象在通过屡次复制之后仍旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被间接转移到老生代中,这种对象从新生代转移到老生代的过程咱们称之为降职。对象降职的条件次要有以下两个(满足其一即可): 对象是否经验过一次Scavenge算法To空间的内存占比是否曾经超过25%默认状况下,咱们创立的对象都会调配在From空间中,当进行垃圾回收时,在将对象从From空间复制到To空间之前,会先查看该对象的内存地址来判断是否曾经经验过一次Scavenge算法,如果地址曾经产生变动则会将该对象转移到老生代中,不会再被复制到To空间。流程图示意: 如果对象没有经验过Scavenge算法,会被复制到To空间,然而如果此时To空间的内存占比曾经超过25%,则该对象依旧会被转移到老生代,如下图所示: 之所以有25%的内存限度是因为To空间在经验过一次Scavenge算法后会和From空间实现角色调换,会变为From空间,后续的内存调配都是在From空间中进行的,如果内存应用过高甚至溢出,则会影响后续对象的调配,因而超过这个限度之后对象会被间接转移到老生代来进行治理。 老生代区:在解说老生代Mark-Sweep(标记革除)和Mark-Compact(标记整顿)算法之前,先来回顾一下援用计数法:对于对象A,任何一个对象援用了A的值,计数器+1,援用生效时计数器-1,当计数器为0时指责回收,然而会存在循环援用的状况,可能会导致内存透露,自2012年起,所有的古代浏览器均放弃了这种算法。 function foo() {//循环援用样例 let a = {}; let b = {}; a.a1 = b; b.b1 = a;}foo();Mark-Sweep(标记革除)算法:Mark-Sweep(标记革除)分为标记和革除两个阶段,在标记阶段会遍历堆中的所有对象,而后标记活着的对象,在革除阶段中,会将死亡的对象进行革除。Mark-Sweep算法次要是通过判断某个对象是否能够被拜访到,从而晓得该对象是否应该被回收,具体步骤如下: 垃圾回收器会在外部构建一个根列表,用于从根节点登程去寻找那些能够被拜访到的变量。比方在JavaScript中,window全局对象能够看成一个根节点。垃圾回收器从所有根节点登程,遍历其能够拜访到的子节点,并将其标记为流动的,根节点不能到达的中央即为非流动的,将会被视为垃圾。垃圾回收器将会开释所有非流动的内存块,并将其归还给操作系统。然而通过标记革除之后的内存空间会⽣产很多不间断的碎⽚空间,这种不间断的碎⽚空间中,在遇到较⼤的对象时可能会因为空间不⾜⽽导致⽆法存储。为了解决内存碎⽚的问题,须要使⽤另外⼀种算法:标记-整顿(Mark-Compact)。标记-整顿(Mark-Compact):标记整顿看待未存活对象不是⽴即回收,⽽是将存活对象挪动到⼀边,而后间接清掉端边界以外的内存。这里为了便于了解,援用两个流程图。 假如在老生代中有A、B、C、D四个对象在垃圾回收的标记阶段,将对象A和对象C标记为流动的在垃圾回收的整顿阶段,将流动的对象往堆内存的一端挪动在垃圾回收的革除阶段,将流动对象左侧的内存全副回收至此就实现了一次老生代垃圾回收的全副过程,然而因为前文提到的「全进展」的存在,在标记阶段同样会妨碍主线程的执行,一般来说,老生代会保留大量存活的对象,如果在标记阶段将整个堆内存遍历一遍,那么势必会造成重大的卡顿。因而,V8引擎有引入了Incremental Marking(增量标记)的概念。Incremental Marking(增量标记): 将本来须要一次性遍历堆内存的操作改为增量标记的形式,先标记堆内存中的一部分对象,而后暂停,将执行权从新交给JS主线程,待主线程工作执行结束后再从原来暂停标记的中央持续标记,直到标记残缺个堆内存。即:把垃圾回收这个⼤的工作分成⼀个个⼩工作,穿插在 JavaScript工作两头执⾏这个理念其实有点像React框架中的Fiber架构,只有在浏览器的闲暇工夫才会去遍历Fiber Tree执行对应的工作,否则提早执行,尽可能少地影响主线程的工作,防止利用卡顿,晋升利用性能。得益于增量标记的益处,V8引擎后续持续引入了提早清理(lazy sweeping)和增量式整顿(incremental compaction),让清理和整顿的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记和并行清理,进一步地缩小垃圾回收对主线程的影响,为利用晋升更多的性能。最初附上V8-GC的触发机制:参考文献及图片出处: 一文搞懂V8引擎的垃圾回收Node —— V8 GC 浅析浅谈V8引擎垃圾回收机制

July 2, 2022 · 1 min · jiezi

关于v8:V8是怎么执行JS代码的

1、V8的演进历史 2008年V8公布第一个版本,过后的V8架构比拟激进,间接将js代码编译为机器码并执行,所以执行速度很快,然而只有Codegen一个编译器,所以对代码的优化很无限。 2010年V8公布了Crankshaft编译器,js代码会先被Full-Codegen编译器编译,如果后续改代码块会被屡次执行,则会用Crankshaft编译器从新编译,生成更优化的代码,之后就应用优化后的代码来执行,进而晋升性能。 Crankshaft编译器对代码的优化无限,所以2015年V8中退出了TurboFan编译器,此时V8仍旧是间接将源码编译为机器码执行,这种架构存在一个外围问题,内存耗费特地大(通常一个几KB的文件,转换为机器码可能就是几十MB,这会小号微小的内存空间)。 2016年V8退出了Ignition编译器,从新引入字节码,旨在缩小内存应用。 2017年V8正式公布全新编译pipeline,它应用Ignition和TurboFan的组合来编译执行代码,从这(V8的5.9版本)开始,晚期的Full-Codegen和Crankshaft编译器不再用来执行js,在最新的架构中,最外围的模块有三个:解析器(Parser)、解释器(Ignition)、优化编译器(TurboFan)。 当V8执行js源码时,首先,解析器会把源码解析为形象语法树(Abstract Syntax Tree),解释器再将AST翻译为字节码,一边解释一边执行,在此过程中,解释器会记录特定代码片段的运行次数,如果运行次数超过了某个阈值,该段代码就被标记为热代码(hot code),并将运行信息反馈给优化编译器(TureboFan),优化编译器依据反馈信息,优化并编译字节码,最终生成优化后的机器码,这样,当该段代码再次被执行时,解释器就间接应用优化后的机器码执行,不必再次解释,从而大大提高了代码运行效率,这种在运行时编译代码的技术叫即时编译(JIT)。 2、V8的解析器将js源码解析为AST,此过程会通过词法剖析、语法分析,通过预解析进步执行效率。 词法剖析:将js源码解析为一个个最小单元的token。 在V8中,Scanner负责接管Unicode字符流,并将其解析为tokens提供给解析器应用。 语法分析:依据语法规定,将tokens组成一个具备前台层级的形象语法树,在这个过程中,如果源码不合乎语法标准,解析过程就会终止,并抛出语法错误。 对于一份js源码,如果所有源码都要通过解析能力执行,那必然会面临三个问题:1、一次性解析所有代码,代码执行工夫变长,2、内存耗费减少,因为解析完的AST以及依据AST编译后的字节码都会寄存在内存中,3、占用磁盘空间,编译后的代码会缓存在磁盘上。 因而,当初支流的浏览器都会进行提早解析,在解析过程中,对于不是立刻执行的函数,只进行预解析(Pre Parser),只有当函数调用时才对函数进行全量解析。进行预解析时,只验证函数的语法是否无效,解析函数申明,确定函数作用域,不生成AST。实现预解析的就是Pre-Parser解析器。 3、V8的解释器Js源码转换为CPU可辨认的机器码,须要耗费微小的内存,V8为了解决内存内存占用问题引入了字节码。字节码是对机器码的形象,语法与汇编有些相似,能够把它看做一个一个的指令。 解析器Ignition依据AST生成字节码并执行。 这个过程中会收集反馈信息,交给TurboFan进行优化编译。TurboFan依据Ignition收集的反馈信息,将字节码编译为优化后的机器码,后续Ignition有优化后的机器码代替字节码执行。 4、V8的优化编译器Ignition解释器在执行字节码时,仍旧须要将字节码转换为机器码,因为CPU只能辨认机器码,尽管多了一层字节码的转换,看起来效率低了,然而相比于机器码,基于字节码能够更不便的进行性能优化,其中最次要的优化就是应用TurboFan编译器编译热点代码。Ignitio解释器在解释执行的过程中,会标记反复执行的热点代码,这些被标记的代码,会被TurboFan编译器编译生成效率更高的机器码。 TurboFan在工作的时候次要用到了两个算法,一个内联,一个是逃逸剖析。 内联就是对嵌套函数进行内联剖析,如下图左侧代码,如果不经优化,间接编译该段代码,则会生成两个函数的机器码,但为了进一步晋升性能,TurboFan就会对这两个函数进行内联,而后在编译,如下提中间代码,更进一步,因为函数外部变量的值都是确定的,所以函数还能够进一步优化,如下图右侧代码。最终生成的机器码相比优化前少了十分多,执行效率天然也就高了。通过内联,能够升高复杂度,打消冗余代码,合并常量,并且,内联技术通常也是逃逸剖析的根底。 逃逸剖析是剖析对象的生命周期是否仅限于以后函数,如果对象是在函数外部定义的,且对象只作用于函数外部,比方对象没有被返回,也没有传递或者给其余函数调用,此时,这个对象会被认为是”未逃逸”的。在编译优化时,会应用标量替换掉未逃逸的对象,以缩小对象定义,从而缩小从内存中拜访对象属性,晋升了执行效率的同时,还缩小了内存的应用。 文章来源于视频:https://www.zhihu.com/zvideo/...

April 1, 2022 · 1 min · jiezi

关于v8:浏览器工作原理和V8引擎

一、浏览器的工作原理 比方在浏览器中输出网址,而后dns进行解析,解析出的就是服务器的一个ip地址。服务器返回一个html文件,浏览器内核在解析html文件的过程中,遇到link标签和script标签援用的css文件和JavaScript文件就会去下载下来。 二、浏览器内核 咱们常常会说:不同的浏览器有不同的内核组成:Gecko:晚期被Netscape和Mozilla Firefox浏览器浏览器应用;Trident:微软开发,被IE4~IE11浏览器应用,然而Edge浏览器曾经转向Blink;Webkit:苹果基于KHTML开发、开源的,用于Safari,Google Chrome之前也在应用;Blink:是Webkit的一个分支,Google开发,目前利用于Google Chrome、Edge、Opera等;等等... 事实上,咱们常常说的浏览器内核指的是浏览器的排版引擎:排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine) 或样版引擎。三、浏览器渲染过程 浏览器内核的 HTML Parse 将 HTML 转化为DOM树(DOM Tree),DOM 的 JavaScript 代码能够对DOM树(DOM Tree)进行操作(JavaScript代码是由JavaScript引擎执行的)。CSS Parse 将css转化为CSS规定(Style Rules)。而后 DOM树(DOM Tree)和CSS规定(Style Rules)通过附加(Attachment)生成渲染树(Render Tree),在 布局引擎(Layout)具体操作下,进行绘制(Painting),浏览器就能够进行展现(Dispaly)。之所以须要布局引擎(Layout),是因为浏览器在不同状态下布局有所不同。 四、意识JavaScript引擎 为什么须要JavaScript引擎呢?咱们后面说过,高级的编程语言都是须要转成最终的机器指令来执行的;事实上咱们编写的JavaScript无论你交给浏览器或者Node执行,最初都是须要被CPU执行的;然而CPU只意识本人的指令集,实际上是机器语言,能力被CPU所执行;所以咱们须要JavaScript引擎帮忙咱们将JavaScript代码翻译成CPU指令来执行; 比拟常见的JavaScript引擎有哪些呢?SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者);Chakra:微软开发,用于IT浏览器;JavaScriptCore:WebKit中的JavaScript引擎,Apple公司开发;V8:Google开发的弱小JavaScript引擎,也帮忙Chrome从泛滥浏览器中怀才不遇;等等… JavaScript是一门高级编程语言:机械语言————>汇编语言————>高级语言五、浏览器内核和JS引擎的关系这里咱们先以WebKit为例,WebKit事实上由两局部组成的: WebCore:负责HTML解析、布局、渲染等等相干的工作;JavaScriptCore:解析、执行JavaScript代码;另外一个弱小的JavaScript引擎就是V8引擎。 六、V8引擎原理 咱们来看一下官网对V8引擎的定义:V8是用C ++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和应用x64,IA-32, ARM或MIPS处理器的Linux零碎上运行。V8能够独立运行,也能够嵌入到任何C ++应用程序中。 V8引擎架构Parse模块会将JavaScript代码转换成AST(形象语法树),这是因为解释器并不间接意识JavaScript代码如果函数没有被调用,那么是不会被转换成AST的。PreParse(预解析),并不是一开始所有代码都须要执行,所以V8引擎就实现了Lazy Parsing(提早解析)的计划,它的作用是将不必要的函数进行预解析,也就是只解析暂 时须要的内容,而对函数的全量解析是在函数被调用时才会进行; Ignition是一个解释器,会将AST转换成ByteCode(字节码)同时会收集TurboFan优化所须要的信息(比方函数参数的类型信息,有了类型能力进行实在的运算); 如果函数只调用一次,Ignition会执行解释执行ByteCode; TurboFan是一个编译器,能够将字节码编译为CPU能够间接执行的机器码如果一个函数被屡次调用,那么就会被标记为热点函数,那么就会通过TurboFan转换成优化的机器码,进步代码的执行性能; 然而,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型产生了变动(比方sum函数原来执行的是 number类型,起初执行变成了string类型),之前优化的机器码并不能正确的解决运算,就会逆向的转换成字节码。 七、执行上下文 <script> var name = 'why' foo(123) function foo (num) { console.log(m) var m = 10 var n = 20 function bar () { console.log(name) } bar() }</script>复制代码全局代码执行前的解析(红色框内) ...

March 11, 2022 · 1 min · jiezi

关于v8:v8

v8常识图谱v8常识图谱 v8的根底环境v8执行流程事件循环系统javascript的设计思维垃圾回收零碎v8的根底环境v8的根底环境 堆空间 树状存储构造存储对象存储闭包函数援用的原生类型栈空间 先进后出存储原生类型全局执行上下文 初始化的this全局作用域全局对象宿主环境 宿主类型浏览器NodeJs其它宿主 内置内置函数内置对象 Chrome.windowNode.global事件循环系统 音讯列表任务调度零碎javascript的设计思维javascript的设计思维 函数是一等公民 函数领有一般类型的个性基于对象设计对象是动静的反对闭包函数表达式类型零碎和垃圾回收 类型零碎垃圾回收作用域 源代码中定义变量的区域动态作用域动静作用域javascript是基于动态作用域的作用域链原型链继承 原型原型链new关键字事件循环系统什么是事件循环系统 JavaScript是单线程模式单线程同时只执行一个工作有新的工作就须要排队执行引入音讯队列音讯队列中的工作就是宏工作任务调度器循环读取音讯队列中的工作分派给指定的工作处理器异步编程 回调函数模式Promise模式await/async模式

February 15, 2022 · 1 min · jiezi

关于v8:教女朋友学前端之深入理解JS引擎

美味值: 口味:番茄肥牛 食堂老板娘:老板,Chrome V8 引擎工作原理面试会问吗? 食堂老板:这块的常识不仅面试可能会问,学会了 JS 引擎的工作原理,能够更好的了解 JavaScript、更好的了解前端生态中 Babel 的词法剖析和语法分析,ESLint 的语法查看原理以及 React、Vue 等前端框架的实现原理。总之,学习引擎原理堪称是一举多得。 食堂老板娘:好好好,别罗嗦了,快开始吧~ 宏观视角看 V8V8 是咱们前端届的网红,它用 C++ 编写,是谷歌开源的高性能 JavaScript 和 WebAssembly 引擎,次要用在 Chrome、Node.js、Electron...中。 在开始讲咱们的配角 V8 引擎之前,先来从宏观视角开展谈谈 V8 所处的地位,建设一个世界观。 在信息科技高速倒退的明天,这个疯狂的大世界充斥着各种电子设备,咱们每天都用的手机、电脑、电子手表、智能音箱以及当初马路上跑的越来越多的电动汽车。 作为软件工程师,咱们能够将它们对立了解为“电脑”,它们都是由中央处理器(CPU)、存储以及输出、输出设备形成。CPU 就像厨师,负责依照菜谱执行命令烧菜。存储如同冰箱,负责保留数据以及要执行的命令(食材)。 当电脑接通电源,CPU 便开始从存储的某个地位读取指令,依照指令一条一条的执行命令,开始工作。电脑还能够接入各种外部设备,比方:鼠标、键盘、屏幕、发动机等等。CPU 不须要全副搞清楚这些设施的能力,它只负责和这些设施的端口进行数据交换就好。设施厂商也会在提供设施时,附带与硬件匹配的软件,来配合 CPU 一起工作。说到这里,咱们便失去了最根底的计算机,也是计算机之父冯·诺伊曼在 1945 年提出的体系结构。 不过因为机器指令人类读起来十分不敌对,难以浏览和记忆,所以人们创造了编程语言和编译器。编译器能够把人类更容易了解的语言转换为机器指令。除此之外,咱们还须要操作系统,来帮咱们解决软件治理的问题。咱们晓得操作系统有很多,如 Windows、Mac、Linux、Android、iOS、鸿蒙等,应用这些操作系统的设施更是不可胜数。为了打消客户端的多样性,实现跨平台并提供对立的编程接口,浏览器便诞生了。 所以,咱们能够将浏览器看作操作系统之上的操作系统,而对于咱们前端工程师最相熟的 JavaScript 代码来说,浏览器引擎(如:V8)就是它的整个世界。 星球最强 JavaScript 引擎毫无疑问,V8 是最风行、最强的 JavaScript 引擎,V8 这个名字的灵感来源于 50 年代经典的“肌肉车”的引擎。 Programming Languages Software AwardV8 也曾取得了学术界的必定,拿到了 ACM SIGPLAN 的 Programming Languages Software Award。 支流 JS 引擎JavaScript 的支流引擎如下所示: ...

August 16, 2021 · 3 min · jiezi

关于v8:走进chrome内心了解V8引擎是如何工作的

作为一个前端程序员,每天下班的第一件事就是关上电脑,情不自禁的点开chrome浏览器,或是摸会儿鱼或是立马进入工作状态。接下来浏览器窗口就会陪伴着你度过一天的时光,失常到七八点钟,晚点就九十点钟,再晚点就陪你跨过一天,时刻关注着你的工作。作为一个虔诚陪伴你的搭档,你扪心自问,你有认真的理解过它是如何工作的吗?你有走进过它的内心世界吗? 如果你也好奇过,那么请收看这期的《走进chrome心田,理解V8引擎是如何工作的》。 V8是什么在深刻理解一件事物之前,首先要晓得它是什么。 V8是一个由Google开源的采纳C++编写的高性能JavaScript和WebAssembly引擎,利用在 Chrome和Node.js等中。它实现了ECMAScript和WebAssembly,运行在Windows 7及以上、macOS 10.12+以及应用x64、IA-32、ARM或MIPS处理器的Linux零碎上。 V8能够独立运行,也能够嵌入到任何C++应用程序中。 V8由来接下来咱们来关怀关怀它如何诞生的,以及为什么叫这个名字。 V8最后是由Lars Bak团队开发的,以汽车的V8发动机(有八个气缸的V型发动机)进行命名,预示着这将是一款性能极高的JavaScript引擎,在2008年9月2号同chrome一起开源公布。 为什么须要V8咱们写的JavaScript代码最终是要在机器中被执行的,但机器无奈间接辨认这些高级语言。须要通过一系列的解决,将高级语言转换成机器能够辨认的的指令,也就是二进制码,交给机器执行。这两头的转换过程就是V8的具体工作。 接下来咱们就来具体的理解一下。 V8组成首先来看一下V8的外部组成。V8的外部有很多模块,其中最重要的4个如下: Parser: 解析器,负责将源代码解析成ASTIgnition: 解释器,负责将AST转换成字节码并执行,同时会标记热点代码TurboFan: 编译器,负责将热点代码编译成机器码并执行Orinoco: 垃圾回收器,负责进行内存空间回收V8工作流程以下是V8中几个重要模块的具体工作流程图。咱们一一剖析。 Parser解析器Parser解析器负责将源代码转换成形象语法树AST。在转换过程中有两个重要的阶段:词法剖析(Lexical Analysis)和语法分析(Syntax Analysis)。 词法剖析也称为分词,是将字符串模式的代码转换为标记(token)序列的过程。这里的token是一个字符串,是形成源代码的最小单位,相似于英语中单词。词法剖析也能够了解成将英文字母组合成单词的过程。词法剖析过程中不会关怀单词之间的关系。比方:词法剖析过程中可能将括号标记成token,但并不会校验括号是否匹配。 JavaScript中的token次要蕴含以下几种: 关键字:var、let、const等 标识符:没有被引号括起来的间断字符,可能是一个变量,也可能是 if、else 这些关键字,又或者是 true、false 这些内置常量 运算符: +、-、 *、/ 等 数字:像十六进制,十进制,八进制以及迷信表达式等 字符串:变量的值等 空格:间断的空格,换行,缩进等 正文:行正文或块正文都是一个不可拆分的最小语法单元 标点:大括号、小括号、分号、冒号等 以下是const a = 'hello world'通过esprima词法剖析后生成的tokens。 [ { "type": "Keyword", "value": "const" }, { "type": "Identifier", "value": "a" }, { "type": "Punctuator", "value": "=" }, { "type": "String", "value": "'hello world'" }]语法分析语法分心是将词法剖析产生的token依照某种给定的模式文法转换成AST的过程。也就是把单词组合成句子的过程。在转换过程中会验证语法,语法如果有错的话,会抛出语法错误。 ...

July 12, 2021 · 1 min · jiezi

关于v8:v8-Heapsnapshot-文件解析

图片起源:debugging-memory-leaks-node-js-applications本文作者:肖思元在 node 中能够通过 v8.getHeapSnapshot 来获取利用以后的堆快照信息,该调用会生成一份 .heapsnapshot 文件,官网并没有对该文件的内容有一个具体的解释,本文将次要对该文件内容进行解析,并演示了一个理解文件内容后能够做的乏味的事件 v8.getHeapSnapshot首先简略回顾下 v8.getHeapSnapshot 是如何应用的: // test.jsconst { writeHeapSnapshot } = require("v8");class HugeObj { constructor() { this.hugeData = Buffer.alloc((1 << 20) * 50, 0); }}// 留神上面的用法在理论利用中通常是 anti-pattern,// 这里只是为了不便演示,才将对象挂到 module 上以避免被 GC 开释module.exports.data = new HugeObj();writeHeapSnapshot();将下面的代码保留到 test.js 中,而后运行 node test.js,会生成文件名相似 Heap.20210228.154141.9320.0.001.heapsnapshot 的文件,该文件能够应用 Chrome Dev Tools 进行查看 对于下面的步骤咱们也能够间接 查看视频演示当咱们将 .heapsnapshot 文件导入到 Chrome Dev Tools 之后,咱们会看到相似上面的内容:上图表格列出了以后堆中的所有对象,其中列的含意是:Constructor,示意对象是应用该函数结构而来Constructor 对应的实例的数量,在 Constructor 前面的 x2 中显示Shallow size,对象本身大小(单位是 Byte),比方下面的 HugeObj,它的实例的 Shallow size 就是本身占用的内存大小,比方,对象外部为了保护属性和值的对应关系所占用的内存,并不蕴含持有对象的大小比方 hugeData 属性援用的 Buffer 对象的大小,并不会计算在 HugeObj 实例的 Shallow size 中Retained size,对象本身大小加上它依赖链路上的所有对象的本身大小(Shallow size)之和Distance,示意从根节点(Roots)达到该对象通过的最短门路的长度heapsnapshot 文件Chrome Dev Tools 只是 .heapsnapshot 文件的一种展示模式,如果咱们心愿最大水平利用这些信息,则须要进一步理解其文件格式咱们能够应用任意的文本编辑器关上该文件,能够发现文件内容其实是 JSON 格局的:因为目前没有具体的阐明文档,前面的内容咱们将联合源码来剖析该文件的内容 ...

March 17, 2021 · 4 min · jiezi

JavaScript深入浅出第4课V8引擎是如何工作的

摘要: 性能彪悍的V8引擎。 《JavaScript深入浅出》系列: JavaScript深入浅出第1课:箭头函数中的this究竟是什么鬼?JavaScript深入浅出第2课:函数是一等公民是什么意思呢?JavaScript深入浅出第3课:什么是垃圾回收算法?JavaScript深入浅出第4课:V8是如何工作的?最近,JavaScript生态系统又多了2个非常硬核的项目。 大神Fabrice Bellard发布了一个新的JS引擎QuickJS,可以将JavaScript源码转换为C语言代码,然后再使用系统编译器(gcc或者clang)生成可执行文件。 Facebook为React Native开发了新的JS引擎Hermes,用于优化安卓端的性能。它可以在构建APP的时候将JavaScript源码编译为Bytecode,从而减少APK大小、减少内存使用,提高APP启动速度。 作为JavaScript程序员,只有极少数人有机会和能力去实现一个JS引擎,但是理解JS引擎还是很有必要的。本文将介绍一下V8引擎的原理,希望可以给大家一些帮助。 JavaScript引擎我们写的JavaScript代码直接交给浏览器或者Node执行时,底层的CPU是不认识的,也没法执行。CPU只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情,比如,我们要计算N阶乘的话,只需要7行的递归函数: function factorial(N) { if (N === 1) { return 1; } else { return N * factorial(N - 1); }}代码逻辑也非常清晰,与阶乘数的学定义完美吻合,哪怕不会写代码的人也能看懂。 但是,如果使用汇编语言来写N阶乘的话,要300+行代码n-factorial.s: 这个N阶乘的汇编代码是我大学时期写的,已经是N年前的事情了,它需要处理10进制与2进制的转换,需要使用多个字节保存大整数,最多可以计算大概500左右的N阶乘。 还有一点,不同类型的CPU的指令集是不一样的,那就意味着得给每一种CPU重写汇编代码,这就很崩溃了。。。 还好,JavaScirpt引擎可以将JS代码编译为不同CPU(Intel, ARM以及MIPS等)对应的汇编代码,这样我们才不要去翻阅每个CPU的指令集手册。当然,JavaScript引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收。 虽然浏览器非常多,但是主流的JavaScirpt引擎其实很少,毕竟开发一个JavaScript引擎是一件非常复杂的事情。比较出名的JS引擎有这些: V8 (Google)SpiderMonkey (Mozilla)JavaScriptCore (Apple)Chakra (Microsoft)IOT:duktape、JerryScript还有,最近发布QuickJS与Hermes也是JS引擎,它们都超越了浏览器范畴,Atwood's Law再次得到了证明: Any application that can be written in JavaScript, will eventually be written in JavaScript.V8:强大的JavaScript引擎在为数不多JavaScript引擎中,V8无疑是最流行的,Chrome与Node.js都使用了V8引擎,Chrome的市场占有率高达60%,而Node.js是JS后端编程的事实标准。国内的众多浏览器,其实都是基于Chromium浏览器开发,而Chromium相当于开源版本的Chrome,自然也是基于V8引擎的。神奇的是,就连浏览器界的独树一帜的Microsoft也投靠了Chromium阵营。另外,Electron是基于Node.js与Chromium开发桌面应用,也是基于V8的。 V8引擎是2008年发布的,它的命名灵感来自超级性能车的V8引擎,敢于这样命名确实需要一些实力,它性能确实一直在稳步提高,下面是使用Speedometer benchmark的测试结果: V8在工业界已经非常成功了,同时它还获得了学术界的肯定,拿到了ACM SIGPLAN的Programming Languages Software Award: V8's success is in large part due to the efficient machine code it generates.Because JavaScript is a highly dynamic object-oriented language, many experts believed that this level of performance could not be achieved. V8's performance breakthrough has had a major impact on the adoption of JavaScript, which is nowadays used on the browser, the server, and probably tomorrow on the small devices of the internet-of-things.JavaScript是一门动态类型语言,这会给编译器增加很大难度,因此专家们觉得它的性能很难提高,但是V8居然做到了,生成了非常高效的machine code(其实是汇编代码),这使得JS可以应用在各个领域,比如Web、APP、桌面端、服务端以及IOT。 ...

July 16, 2019 · 3 min · jiezi

V8引擎浅析Chrome-V8引擎中的垃圾回收机制和内存泄露优化策略

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。 一、前言V8的垃圾回收机制:JavaScript使用垃圾回收机制来自动管理内存。垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带来的内存泄露问题。 但使用了垃圾回收即意味着程序员将无法掌控内存。ECMAScript没有暴露任何垃圾回收器的接口。我们无法强迫其进 行垃圾回收,更无法干预内存管理 内存管理问题:在浏览器中,Chrome V8引擎实例的生命周期不会很长(谁没事一个页面开着几天几个月不关),而且运行在用户的机器上。如果不幸发生内存泄露等问题,仅仅会 影响到一个终端用户。且无论这个V8实例占用了多少内存,最终在关闭页面时内存都会被释放,几乎没有太多管理的必要(当然并不代表一些大型Web应用不需 要管理内存)。但如果使用Node作为服务器,就需要关注内存问题了,一旦内存发生泄漏,久而久之整个服务将会瘫痪(服务器不会频繁的重启)。 二、chrome内存限制 2.1存在限制Chrome限制了所能使用的内存极限(64位为1.4GB,32位为1.0GB),这也就意味着将无法直接操作一些大内存对象。 2.2为何限制Chrome之所以限制了内存的大小,表面上的原因是V8最初是作为浏览器的JavaScript引擎而设计,不太可能遇到大量内存的场景,而深层次的原因 则是由于V8的垃圾回收机制的限制。由于V8需要保证JavaScript应用逻辑与垃圾回收器所看到的不一样,V8在执行垃圾回收时会阻塞 JavaScript应用逻辑,直到垃圾回收结束再重新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。 若V8的堆内存为1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。这样浏览器将在1s内失去对用户的响 应,造成假死现象。如果有动画效果的话,动画的展现也将显著受到影响 三、chrome V8的堆构成V8的堆其实并不只是由老生代和新生代两部分构成,可以将堆分为几个不同的区域: 1、新生代内存区:大多数的对象被分配在这里,这个区域很小但是垃圾回特别频繁; 2、老生代指针区:属于老生代,这里包含了大多数可能存在指向其他对象的指针的对象,大多数从新生代晋升的对象会被移动到这里; 3、老生代数据区:属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针; 4、大对象区:这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象; 5、代码区:代码对象,也就是包含JIT之后指令的对象,会被分配在这里。唯一拥有执行权限的内存区; 6、Cell区、属性Cell区、Map区:存放Cell、属性Cell和Map,每个区域都是存放相同大小的元素,结构简单。 每个区域都是由一组内存页构成,内存页是V8申请内存的最小单位,除了大对象区的内存页较大以外,其他区的内存页都是1MB大小,而且按照1MB对 齐。内存页除了存储的对象,还有一个包含元数据和标识信息的页头,以及一个用于标记哪些对象是活跃对象的位图区。另外每个内存页还有一个单独分配在另外内 存区的槽缓冲区,里面放着一组对象,这些对象可能指向其他存储在该页的对象。垃圾回收器只会针对新生代内存区、老生代指针区以及老生代数据区进行垃圾回收。 四、chrome V8的垃圾回收机制4.1如何判断回收内容如何确定哪些内存需要回收,哪些内存不需要回收,这是垃圾回收期需要解决的最基本问题。我们可以这样假定,一个对象为活对象当且仅当它被一个根对象 或另一个活对象指向。根对象永远是活对象,它是被浏览器或V8所引用的对象。被局部变量所指向的对象也属于根对象,因为它们所在的作用域对象被视为根对 象。全局对象(Node中为global,浏览器中为window)自然是根对象。浏览器中的DOM元素也属于根对象。 4.2如何识别指针和数据垃圾回收器需要面临一个问题,它需要判断哪些是数据,哪些是指针。由于很多垃圾回收算法会将对象在内存中移动(紧凑,减少内存碎片),所以经常需要进行指针的改写: 目前主要有三种方法来识别指针: 保守法:将所有堆上对齐的字都认为是指针,那么有些数据就会被误认为是指针。于是某些实际是数字的假指针,会背误认为指向活跃对象,导致内存泄露(假指针指向的对象可能是死对象,但依旧有指针指向——这个假指针指向它)同时我们不能移动任何内存区域。编译器提示法:如果是静态语言,编译器能够告诉我们每个类当中指针的具体位置,而一旦我们知道对象时哪个类实例化得到的,就能知道对象中所有指针。这是JVM实现垃圾回收的方式,但这种方式并不适合JS这样的动态语言标记指针法:这种方法需要在每个字末位预留一位来标记这个字段是指针还是数据。这种方法需要编译器支持,但实现简单,而且性能不错。V8采用的是这种方式。V8将所有数据以32bit字宽来存储,其中最低一位保持为0,而指针的最低两位为014.3 V8回收策略自动垃圾回收算法的演变过程中出现了很多算法,但是由于不同对象的生存周期不同,没有一种算法适用于所有的情况。所以V8采用了一种分代回收的策 略,将内存分为两个生代:新生代和老生代。 新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。分别对新生代和老生代使用 不同的垃圾回收算法来提升垃圾回收的效率。对象起初都会被分配到新生代,当新生代中的对象满足某些条件(后面会有介绍)时,会被移动到老生代(晋升)。 五、新生代算法新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。在Scavenge的具体实现中,主要是采用一种复制的方式的方法--cheney算法。 在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。 ...

June 26, 2019 · 2 min · jiezi

JavaScript-究竟是如何工作的第二部分

原文地址:How Does JavaScript Really Work? (Part 2)原文作者:Priyesh Patel 照片来源于Unsplash上的Samuel Zeller 在这篇文章的第一部分,我简要概述了编程语言的一般工作机制,并深入探讨了 V8 引擎的管道。第二部分将介绍一些更重要的概念,这些概念是每一个 JavaScript 程序员都必须了解的,并且不仅仅和 V8 引擎有关。对于任何一个程序员来说,最关注的两个问题无非就是:时间复杂度和空间复杂度。第一部分介绍了 V8 为改进 JavaScript 执行时间所做的速度提升和优化,第二部分则将着重介绍内存管理方面的知识。 内存堆 Orinoco 的 logo:V8 的垃圾回收器 每当你在 JavaScript 程序中定义了一个变量、常量或者对象时,你都需要一个地方来存储它。这个地方就是内存堆。当遇到语句 var a = 10 的时候,内存会分配一个位置用于存储 a 的值可用内存是有限的,而复杂的程序可能有很多变量和嵌套对象,因此合理地使用可用内存非常重要。和诸如 C 这种需要显式分配和释放内存的语言不同,JavaScript 提供了自动垃圾回收机制。一旦对象/变量离开了上下文并且不再使用,它的内存就会被回收并返还到可用内存池中。在 V8 中,垃圾回收器的名字叫做 Orinoco,它的处理过程非常高效。这篇文章有相关解释。标记与清除算法 标记和清除算法 我们通常会使用这种简单有效的算法来判定可以从内存堆中安全清除的对象。算法的工作方式正如其名:将对象标记为可获得/不可获得,并将不可获得的对象清除。垃圾回收器周期性地从根部或者全局对象开始,移向被它们引用的对象,接着再移向被这些对象引用的对象,以此类推。所有不可获得的对象会在之后被清除。 内存泄漏虽然垃圾回收器很高效,但是开发者不应该就此将内存管理的问题束之高阁。管理内存是一个很复杂的过程,哪一块内存不再需要并不是单凭一个算法就能决定的。内存泄漏指的是,程序之前需要用到部分内存,而这部分内存在用完之后并没有返回到内存池。下面是一些会导致你的程序出现内存泄漏的常见错误:全局变量:如果你不断地创建全局变量,不管有没有用到它们,它们都将滞留在程序的整个执行过程中。如果这些变量是深层嵌套对象,将会浪费大量内存。 var a = { ... }var b = { ... }function hello() { c = a; // 这是一个你没有意识到的全局变量}如果你试图访问一个此前没有声明过的变量,那么将在全局作用域中创建一个变量。在上面的例子中,c 是没有使用 var 关键字显式创建的变量/对象。 ...

May 29, 2019 · 1 min · jiezi

今日头条网红题-????-async-functions-和-promises哪个更快

题目如下 async function async1() { console.log('async1 start') await async2() console.log('async1 end')}async function async2() { console.log('async2')}console.log('script start')setTimeout(function() { console.log('setTimeout') }, 0) async1()new Promise(function(resolve) { console.log('promise1') resolve()}).then(function() { console.log('promise2')})console.log('script end')而v8和node10产出的结果有所不同。 v8运行结果???? node10运行结果???? 先说下async/await原理???? async 声明的函数,其返回值必定是 promise 对象,如果没有显式返回 promise 对象,也会用 Promise.resolve() 对结果进行包装,保证返回值为 promise 类型await 会先执行其右侧表达逻辑(从右向左执行),并让出主线程,跳出 async 函数,而去继续执行 async 函数外的同步代码如果 await 右侧表达逻辑是个 promise,让出主线程,继续执行 async 函数外的同步代码,等待同步任务结束后,且该promise 被 resolve 时,继续执行 await 后面的逻辑如果 await 右侧表达逻辑不是 promise 类型,那么 async 函数之外的同步代码执行完毕之后,会回到 async函数内部,继续执行 await 之后的逻辑-- 摘自 LucasHC ...

May 29, 2019 · 1 min · jiezi

从Google-V8引擎剖析Promise实现

从Google V8引擎剖析Promise实现 本文阅读的源码为Google V8 Engine v3.29.45,此版本的promise实现为js版本,在后续版本Google继续对其实现进行了处理。引入了es6语法等,在7.X版本迭代后,逐渐迭代成了C版本实现。 贴上源码地址:https://chromium.googlesource... 大家自觉传送。 代码中所有类似%functionName的函数均是C语言实现的运行时函数。 Define variables首先定义了将要在JS作用域使用了一些变量,提高了编译器的效率。 var IsPromise;var PromiseCreate;var PromiseResolve;var PromiseReject;var PromiseChain;var PromiseCatch;var PromiseThen;var PromiseHasRejectHandler;随后定义了一些全局私有变量供给和C语音交互,用于维护Promise的状态和进行Debug。 var promiseStatus = GLOBAL_PRIVATE("Promise#status");var promiseValue = GLOBAL_PRIVATE("Promise#value");var promiseOnResolve = GLOBAL_PRIVATE("Promise#onResolve");var promiseOnReject = GLOBAL_PRIVATE("Promise#onReject");var promiseRaw = GLOBAL_PRIVATE("Promise#raw");var promiseDebug = GLOBAL_PRIVATE("Promise#debug");var lastMicrotaskId = 0;其中GLOBAL_PRIVATE是python进行实现的,运用python的宏定义(macro)来定义调用了C语言的CreateGlobalPrivateOwnSymbol方法。 macro GLOBAL_PRIVATE(name) = (%CreateGlobalPrivateOwnSymbol(name));随后运用了一个自执行的匿名函数进行闭包逻辑。 (function() { // 主逻辑})();在闭包逻辑的最后,在promise原型上挂载了三个方法:chain,then,catch。在promise对象上挂载了all,race等六个方法。将Promise对象注册到了global。 %AddNamedProperty(global, 'Promise', $Promise, DONT_ENUM);InstallFunctions($Promise, DONT_ENUM, [ "defer", PromiseDeferred, "accept", PromiseResolved, "reject", PromiseRejected, "all", PromiseAll, "race", PromiseOne, "resolve", PromiseCast]);InstallFunctions($Promise.prototype, DONT_ENUM, [ "chain", PromiseChain, "then", PromiseThen, "catch", PromiseCatch]);Start from constructorvar $Promise = function Promise(resolver) { // 如果传入参数为全局promiseRaw变量的时候return if (resolver === promiseRaw) return; // 如果当前函数不是构造函数的化,抛出错误这不是一个promise if (!%_IsConstructCall()) throw MakeTypeError('not_a_promise', [this]); // 如果传入参数不是一个函数的话,抛出错误,传入参数不是一个function if (!IS_SPEC_FUNCTION(resolver)) throw MakeTypeError('resolver_not_a_function', [resolver]); var promise = PromiseInit(this); try { // debug相关忽略 %DebugPushPromise(promise); resolver(function(x) { PromiseResolve(promise, x) }, function(r) { PromiseReject(promise, r) }); } catch (e) { // 报错之后走到错误处理函数 PromiseReject(promise, e); } finally { // debug相关忽略 %DebugPopPromise(); }}构造函数在做完额外的异常和参数判断后,进入主逻辑调用PromiseInit方法初始化promise,随后调用了resolver方法,传入了两个默认的处理函数。在promise在内部被调用时(PromiseDeferred方法被调用时)会实例化$promise,将默认方法return回去,使得创建的promise示例具有resolve和reject方法。 ...

May 22, 2019 · 4 min · jiezi

Whats-New-in-JavaScript

前几天 Google IO 上 V8 团队为我们分享了《What's New in JavaScript》主题,分享的语速很慢推荐大家可以都去听听就当锻炼下听力了。看完之后我整理了一个文字版帮助大家快速了解分享内容,嘉宾主要是分享了以下几点: JS 解析快了 2 倍async 执行快了 11 倍平均减少了 20% 的内存使用class fileds 可以直接在 class 中初始化变量不用写在 constructor 里私有变量前缀string.matchAll 用来做正则多次匹配numeric seperator 允许我们在写数字的时候使用 _ 作为分隔符提高可读性bigint 新的大数字类型支持Intl.NumberFormat 本地化格式化数字显示Array.prototype.flat(), Array.prototype.flatMap() 多层数组打平方法Object.entries() 和 Object.fromEntries() 快速对对象进行数组操作globalThis 无环境依赖的全局 this 支持Array.prototype.sort() 的排序结果稳定输出Intl.RelativeTimeFormat(), Intl.DateTimeFormat() 本地化显示时间Intl.ListFormat() 本地化显示多个名词列表Intl.locale() 提供某一本地化语言的各种常量查询顶级 await 无需写 async 的支持Promise.allSettled() 和 Promise.any() 的增加丰富 Promise 场景WeakRef 类型用来做部分变量弱引用减少内存泄露Async 执行比之前快了11倍开场就用 11x faster 数字把大家惊到了,也有很多同学好奇到底是怎么做到的。其实这个优化并不是最近做的,去年11月的时候 V8 团队就发了一篇文章 《Faster async functions and promises》,这里面就非常详尽的讲述了如何让 async/await 优化到这个速度的,其主要归功于以下三点: ...

May 11, 2019 · 3 min · jiezi

译JavaScript-究竟是如何工作的第一部分

原文地址:How Does JavaScript Really Work? (Part 1)原文作者:Priyesh Patel译者:Chor 如果你是一个 JS 开发者或者是正在学习这门语言的学生,很大概率上你会遇到双字母词"V8"。在这篇文章中,我将会为你简述不同的 JS 引擎并深入探究 V8 引擎的工作机制。文章的第二部分涵盖了内存管理的概念,不久后将发布。 这篇文章是由 Bit (GitHub) 带来的。作为一个共享组件的平台,Bit 帮助每个人构建模块化的 JavaScript 应用程序,在项目和团队之间轻松地共享组件,同时实现更好&更快的构建。试试看。 编程语言是如何工作的?在开始讲解 JavaScript 之前,我们首先要理解任意一门编程语言的基本工作方式。电脑是由微处理器构成的,我们通过书写代码来命令这台小巧但功能强大的机器。但是微处理器能理解什么语言?它们无法理解 Java,Python 等语言,而只懂机器码。用机器语言或汇编语言编写企业级代码是不可行的,因此我们需要像 Java,Python 这样配带一个解释器或者编译器用于将其转换为机器码的高级语言。 编译器和解释器编译器/解释器可以用它处理的语言或任何其他语言来编写。 解释器: 一行一行地快速读取和翻译文件。这就是 JavaScript 最初的工作原理。 编译器: 编译器提前运行并创建一个文件,其中包含了输入文件的机器码转换。 有两种途径可以将 JavaScript 代码转换为机器码。编译代码时,机器对代码开始运行前将要发生的事情有更好的理解,这将加快稍后的执行速度。不过,在这个过程之前需要花费时间。 另一方面,解释代码时,执行是立即的,因此要更快,但是缺乏优化导致它在大型应用程序下运行缓慢。 创建 ECMAScript 引擎的人很聪明,他们集二者之长开发了 JIT(Just-in-time) 编译器。JavaScript 同时被编译和解释,但实际实现和顺序取决于引擎。我们将会看到 V8 团队采用的是什么策略。 从 JavaScript 到机器码就 JavaScript 而言,有一个引擎将其转换为机器码。和其他语言类似,引擎可以用任何语言来开发,因此这样的引擎不止一个。 V8 是谷歌针对 Chorme 浏览器的引擎实现。SpiderMonkey 是第一个引擎,针对网景浏览器开发,现用于驱动 FireFox。JavaScriptCore 是苹果针对 Safari 浏览器使用的引擎。还有很多,如果你想知道 Internet Explorer 背后的引擎,查看这个维基百科页面. ...

May 10, 2019 · 1 min · jiezi

Node12有哪些值得关注的新特性

前言时隔一年,Node.js 12 如约而至,正式发布第一个 Current 版本。它将从2019年10月开始进入长期支持(LTS)版本直到2022年4月。 该版本带来的新特性: V8 更新带来好多不错的特性。HTTP 解析速度提升。启动速度大幅提升。更好的诊断报告和堆分析工具。ESM 模块更新。LTS Node维护了着两条发布流程线:奇数版本每年的10月份发布,偶数版本第二年的4月份发布。当一个奇数版本发布后,最近的一个偶数版本会立即进入LTS维护计划,一直持续18个月(LTS Start )。再之后会有12个月的延长维护期(Maintenance Start)。 这两个期间可以支持的变更是不一样的: LTS期间支持的变更:bug fix,安全问题 fix, 文档更新和与未来新特性兼容问题的更新。Maintenanece期间支持的变更: 严重的bug fix,严重的安全问题 fix 或者文档更新。当一个偶数版本发布时,奇数版本只有2个月的维护期,再只能乖乖升级。就目前而言,Node.js 6.x 和 8.x 将在 2019 年末结束 LTS 的支持,大家尽快升级到 10.x 吧。 ESM自从ES6中的标准化以来,import/ exportsyntax已成为JavaScript开发人员的首选模块语法,而Node团队一直在努力实现本机化。实验性支持从第8阶段的Node 8.0开始,并在最新的Node版本上迈出了重要的一步。所有主流浏览器都支持 ECMAScript模块<script type="module">,因此这是Node的一个巨大更新。 // default exportsimport module from 'module'// named exportsimport { namedExport } from 'module'// namespace exportsimport * as module from 'module'V8引擎更新到7.4本次版本更新,也带几个不错的特性: 异步堆栈跟踪参数调用不匹配时的调用速度优化更快的 JavaScript 解析速度更快的 awaitECMAScript 新特性支持随着 V8 的更新,很多 ES 的新规范也落地了,比如支持类的私有变量。 ...

April 26, 2019 · 1 min · jiezi

干货:浏览器渲染引擎Webkit和V8引擎工作原理

浏览器的历史W3C再80年代后期90年代初期发明了世界上第一个浏览器WorldWideWeb(后更名为Nexus),支持文本/简单的样式表/电影/声音和图片1993年,网景(netscape)浏览器诞生,没有JavaScript,没有css,只显示简单的html元素1995年,微软推出闻名世界的IE浏览器,自此第一次浏览器大战打响,IE受益于Windows系统获得空前的成功,逐渐取代网景浏览器1998年处于低谷的网景成立了Mozilla基金会,在该基金会推动下,开发了著名的开源项目Firefox并在2004年发布1.0版本,拉开了第二次浏览器大战的序幕,IE发展更新较缓慢,Firefox一推出就深受大家的喜爱,市场份额一直上升。在Firefox浏览器发布1.0版本的前一年,2003年,苹果发布了Safari浏览器,并在2005年释放了浏览器中一种非常重要部件的源代码,发起了一个新的开源项目WebKit2008年,Google以苹果开源项目WebKit作为内核,创建了一个新的项目Chromium,在Chiromium的基础上,Google发布了ChromeWebkit模块和其依赖模块上图是WebKit模块和其依赖模块的关系。在操作系统之上的是WebKit赖以工作的众多第三方库,如何高效使用它们是WebKit和各大浏览器厂商的一个重大课题。WebCore部分都是加载和渲染的基础部分WebKit Ports是WebKit非共享部分,对于不同浏览器移植中由于平台差异/依赖的第三方库和需求不同等方面原因,往往按照自己的方式来设计和实现。在WebCore/js引擎/WebKitPorts之上主要是提供嵌入式编程接口,提供给浏览器调用。页面加载解析渲染过程简介如上图所示,图中虚线是与底层第三方库交互。当访问一个页面的时候,会利用网络去请求获取内容,如果命中缓存了,则会在存储上直接获取;如果内容是个HTML格式,首先会找到html解释器进行解析生成DOM树,解析到style的时候会找到css解释器工作得到CSSOM,解析到script会停止解析并开始解析执行js脚本;DOM树和CSSOM树会构建成一个render树,render树上的节点不和DOM树一一对应,只有显示节点才会存在render树上;render树已经知道怎么绘制了,进入布局和绘图,绘制完成后将调用绘制接口,从而显示在屏幕上。从资源的字节流到DOM树上面我们简单介绍了整个过程,现在我们开始认识一下各个步骤的具体过程。字节流经过解码后是字符流,然后通过词法分析器会被解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点组成一棵DOM树 词法分析 在进行词法分析前,解释器首先要检查网页内容实用的编码格式,找到合适的解码器,将字节流转换成特定格式的字符串,然后交给词法分析器进行分析。 每次词法分析器都会根据上次设置的内部状态和上次处理后的字符串来生成一个新的词语。内部使用了超过70种状态。(ps:生成的词语还会进过XssAutitor验证词语是否安全合法,非合法的词语不能通过) 如上图所示,举个例子<div> <img src="/a" /></div>1 接收到"<“进入TagOpen状态 2 接收"d”,根据当前状态是TagOpen判断进入TagName状态,之后接收"i"/“v” 3 接收">“进入TagEnd状态,此时得到了div的开始标签(StartTag) 4 接收”<“进入TagOpen,接收"img"后接收到空格得到了img开始标签 6 进入attribute一系列(笔者自己命名的,不知道叫啥)状态,得到了src属性吗和”/a"属性值 6 同样方式获得div结束标签 词语到节点 得到词语(Tokens)后,就可以开始形成DOM节点了。 注意:这里说的节点不单单指HTMLElement节点,还包括TextNode/Attribute等等一系列节点,它们都继承自Node类。 词语类型只有6种,DOCTYPE/StartTag/EndTag/Comment/Character/EndOfFile组成DOM树 因为节点都可以看成有开始和结束标记,所以用栈的结构来辅助构建DOM树再合适不过了。 当遇到开始标记的时候,推入栈,当遇到结束标记的时候,退栈放再DOM树上,再拿上述的html代码做例子。<div> <img src="/a" /> <span>webkit</span></div>1 遇到div开始标签,将div推入栈; 2 遇到img开始标签,将img推入栈; 3 遇到src属性,将src推入栈; 4 将src从栈中取出,作为DOM树的一部分; 5 遇到img结束标签,说明img包裹着src属性,取出img,作为src的父亲节点; 6 遇到span开始标签,将span推入栈; 7 遇到文本webkit,将文本推入栈; 8 取出webkit文本,待分发; 9 遇到span结束标签,说明span标签包裹着webkit文案,取出span标签,作为文本webkit的父亲节点; 10 遇到div结束标签,取出div标签,说明div标签包裹着img和span,作为它们的公共父亲节点CSS解析WebKit 使用 Flex 和 Bison 解析器生成器,通过 CSS 语法文件自动创建解析器。最后WebKit将创建好的结果直接设置到StyleSheetContents对象中。规则匹配 当WebKit需要为HTML元素创建RenderObject类(后面会讲到)的时候,首先会先去获取样式信息,得到RenderStyle对象——包含了匹配完的结果样式信息。 根据元素的标签名/属性检查规则,如果某个规则匹配上该元素,Webkit把这些规则保存在匹配结果中 最后Webkit对这些规则进行排序,整合,将样式属性值返回脚本设置CSS CSSOM在DOM中的一些节点接口加入了获取和操作css属性或者接口的JavaScript接口,因而JavaScript可以动态操作css样式。 CSSOM定义了样式表的接口CSSStyleSheet,document.styleshheets可以查看当前网页包含的所有css样式表 W3C定义了另外一个规范,CSSOM View,增加一些新的属性到Window.Document,Element.MounseEvent等接口,这些CSS的属性能让JavaScript获取视图信息至此我们已经了解到了文档的解析过程,这里有一些实验可以帮助你更好的了解页面加载过程发生了什么。聊聊浏览器的渲染机制——若邪Y布局只要发生样式的改变,都会触发检查是否需要布局计算当首次加载页面/renderStyle改变/滚动操作的时候,都会触发布局布局是比较耗时的操作,更糟糕的时候布局的下一步就是渲染,我们可以通过硬件加速来跳过布局和渲染,下面我们会讲到。多进程的浏览器一个好的程序常常被划分为几个相互独立又彼此配合的模块,浏览器也是如此,以 Chrome 为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能,每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。具体说来,Chrome 的主要进程及其职责如下:Browser Process:负责包括地址栏,书签栏,前进后退按钮等部分的工作;负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;Renderer Process:负责一个 tab 内关于网页呈现的所有事情Plugin Process:负责控制一个网页用到的所有插件,如 flashGPU Process负责处理 GPU 相关的任务通过「页面右上角的三个点点点 — 更多工具 — 任务管理器」即可打开相关面板 加载页面各进程的合作 处理输入UI thread 需要判断用户输入的是 URL 还是 query;开始导航当用户点击回车键,UI thread 通知 network thread 获取网页内容,并控制 tab 上的 spinner 展现,表示正在加载中。读取响应当请求响应返回的时候,network thread 会依据 Content-Type 及 MIME Type sniffing 判断响应内容的格式如果响应内容的格式是 HTML ,下一步将会把这些数据传递给 renderer process,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。查找渲染进程当上述所有检查完成,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。确认导航进过了上述过程,数据以及渲染进程都可用了, Browser Process 会给 renderer process 发送 IPC 消息来确认导航,一旦 Browser Process 收到 renderer process 的渲染确认消息,导航过程结束,页面加载过程开始。此时,地址栏会更新,展示出新页面的网页信息。history tab 会更新,可通过返回键返回导航来的页面,为了让关闭 tab 或者窗口后便于恢复,这些信息会存放在硬盘中。渲染DOM树构建完成之后,Webkit还要为DOM树构建RenderObject树。什么情况下会为一个DOM节点建立新的RenderObject对象呢1.ducument节点2.可视节点,例如html,body,div等。而WebKit不会为非可视化节点创建RenderObject节点,例如link,head,script3.某些情况下WebKit会建立匿名的RenderObject,该RenderObject不对应DOM树的任何节点,例如匿名的RenderBlocktip:如果一个节点即包含块级节点又包含内联节点,会为内联节点创建一个RenderBlock,即形成RenderObject——RenderObject ——RenderBlock——RenderObject网页是可以分层的,可以让WebKit在渲染处理上获得便利。会产生RenderLayer的情况: document节点和html节点 显示定义position属性的RenderObject 节点有overflow/alpha等效果RenderObject 使用canvas2d或者webgl技术,(注:canvas节点创建的时候不会马上生成RenderLayer对象,在js创建了2d或者3d上下文的时候才创建 Video节点对应的RenderObjectRenderObject对象知道如何绘制自己了,需要调用绘图上下文来进行绘图操作。渲染方式:软件渲染(Cpu完成)和硬件加速渲染(Gpu完成)软件渲染Renderer进程消息循环调用判断是否需要重新计算的布局和更新,如要 Renderer进程创建共享内存 WebKit计算重绘区域中重叠的RenderLayer,RenderLayer重新绘制,绘制结果到共享内存的位图中 绘制完成后,Renderer进程发生消息给Browser进程,Browser进程将更新的区域将共享内存的内容绘制到自己对应存储区域中(绘制过程不会影响该网页结果的显示) Browser进程回复消息给Renderer,回收共享内存 Browser进程绘制到窗口硬件渲染 GPU硬件进行绘图和合成,每个网页的Renderer进程都是将之前介绍的3D绘图和合成操作传递给GPU进程,由它来统一调度 和执行,在安卓中,GPU进程并不存在,WebKit将所有工作放在Browser进程中的一个线程完成。 GPU进程处理一些命令后,会向Renderer进程报告自己当前的状态,Renderer进程通过检查状态信息和自己的期望结果来确定是否满足自己的条件。GPU进程最终绘制的结果不再像软件渲染那样通过共享内存的方式传递给Browser进程,而是直接将页面的内容绘制在浏览器的标签窗口理想情况,每一个层都会有个存储区域,保存绘图结果,最后将这些层的内容合并(compositing)软件渲染机制是没有合成阶段的,软件渲染的结果是一个位图(bitmap),绘制每一层的时候都使用该位图,区别在于绘制的位置可能不一样,每一层按照从后前的顺序。这样软件绘图使用的只是一块内存空间即可。软件渲染只能处理2D方面的操作,并且在高fps的绘图中性能不好,比如视频和canvas2d等,但是cpu使用的缓存机制有效减少了重复绘制的开销硬件绘制和所有的层的合成都使用Gpu完成,硬件加速渲染能支持现在所有的html5定义的2d和3d绘图标准;另外,由于软件渲染没有为每一层提供后端存储,因而需要将和某区域有重叠部分的所有层次相关区域重新绘制一次,而硬件加速渲染只需重新绘制更新发生的层次。实验时间<div id=“box”></div><div id=“bo2”></div> #box { position: relative; width: 100px; height: 100px; background: #ccc; transform: translate3d(0,0,0); transition: transform 2s linear; } #box.move { transform: translate3d(100px,0,0) !important } #box2 { position: relative; width: 100px; height: 100px; background: #ccc; left: 0; transition: left 2s linear; } #box2.move { left: 100px !important }var box2 = document.getElementById(‘box2’)setTimeout(() => { box2.classList.add(‘move’)}, 200);首先我们看下利用开发者工具Layers可以看到,如下图,box1利用了transform3d,从而判断需要为box1独立一层,而其他的内容则依旧附在document层。我们切换到performance进行录制,查看event log如下图。发现在box2在移动的时候,不断重复5各过程:recalculate style——layout——update layer tree——paint——composite layers也就是说document层不断得重新计算布局,重新渲染,再和box2合并layers,这造成了巨大的浪费。我们接下来来看一些box1的移动。var box = document.getElementById(‘box1’)setTimeout(() => { box.classList.add(‘move’)}, 200);如下图,在box1移动的时候,没有了布局和绘制的过程,利用CSS3D加速,只需要在合并层之前改变属性,再次合并层就可以了,不需要重新布局,也没有绘制步骤,这就是为什么我们在写动画的时候要时候3d启用硬件加速的原因,大大减少了布局绘制的资源浪费。V8引擎上面我们已经把渲染过程了解清楚了,接下来来看一下V8引擎这个重头戏吧~!V8引擎和渲染引擎通信当用户在屏幕上触发诸如 touch 等手势时,首先收到手势信息的是 Browser process, 不过 Browser process 只会感知到在哪里发生了手势,对 tab 内内容的处理是还是由渲染进程控制的。事件发生时,浏览器进程会发送事件类型及相应的坐标给渲染进程,渲染进程随后找到事件对象,交给js引擎处理,如果js代码中利用了侨界接口将该节点绑定了事件监听,那么就会触发该事件监听函数。字节码 机器码 JIT编译型语言如c/c++,处理该语言实际上使用编译器直接将它们编译成本地代码,用户知识使用这些变异号的本地代码,被系统的加载起加载执行,这些本地代码由操作系统调度CPU直接执行java做法是明显的两个阶段,首先是编译,不像c/c++编译成本地代码,而是编译生成字节码,字节码是跨平台的中间表示,然后java虚拟机加载字节码,使用解释器执行这些代码。V8之前的版本直接的将抽象语法树通过JIT技术转换成本地代码,放弃了在字节码阶段可以进行的一些性能优化,但保证了执行速度。在V8生成本地代码后,也会通过Profiler采集一些信息,来优化本地代码。虽然,少了生成字节码这一阶段的性能优化,但极大减少了转换时间。但是在2017年4月底,v8 的 5.9 版本发布了,新增了一个 Ignition 字节码解释器,将默认启动(主要动机)减轻机器码占用的内存空间,即牺牲时间换空间 提高代码的启动速度故事得从 Chrome 的一个 bug 说起: http://crbug.com/593477 。Bug 的报告人发现,当在 Chrome 51 (canary) 浏览器下加载、退出、重新加载 facebook 多次,并打开 about:tracing 里的各项监控开关,可以发现第一次加载时 v8.CompileScript 花费了 165 ms,再次加载加入 V8.ParseLazy 居然依然花费了 376 ms。按说如果 Facebook 网站的 js 脚本没有变,Chrome 的缓存功能应该缓存了对 js 脚本的解析结果,不该花费这么久。这是为什么呢?这就是之前 v8 将 JS 代码编译成机器码所带来的问题。因为机器码占空间很大,v8 没有办法把 Facebook 的所有 js 代码编译成机器码缓存下来,因为这样不仅缓存占用的内存、磁盘空间很大,而且再次进入时序列化、反序列化缓存所花费的时间也很长,时间、空间成本都接受不了。在启动速度方面,如今内存占用过大的问题消除了,就可以提前编译所有代码了。因为前端工程为了节省网络流量,其最终 JS 产品往往不会分发无用的代码,所以可以期望全部提前编译 JS 代码不会因为编译了过多代码而浪费资源。v8 对于 Facebook 这样的网站就可以选择全部提前编译 JS 代码到字节码,并把字节码缓存下来,如此 Facebook 第二次打开的时候启动速度就变快了。下图是旧的 v8 的执行时间的统计数据,其中 33% 的解析、编译 JS 脚本的时间在新架构中就可以被缩短。v8 自身的重构方面,有了字节码,v8 可以朝着简化的架构方向发展,消除 Cranshaft 这个旧的编译器,并让新的 Turbofan 直接从字节码来优化代码,并当需要进行反优化的时候直接反优化到字节码,而不需要再考虑 JS 源代码。最终达到如下图所示的架构。其实,Ignition + TurboFan 的组合,就是字节码解释器 + JIT 编译器的黄金组合。这一黄金组合在很多 JS 引擎中都有所使用,例如微软的 Chakra,它首先解释执行字节码,然后观察执行情况,如果发现热点代码,那么后台的 JIT 就把字节码编译成高效代码,之后便只执行高效代码而不再解释执行字节码。隐藏类在V8中建立类有两个主要的理由,即(1)将属性名称相同的对象归类,及(2)识别属性名称不同的对象。同一类中的对象有完全相同的对象描述,而这可以加速属性存取。在V8,符合归类条件的类会配置在各种JavaScript对象上。对象引用所配置的类。然而这些类只存在于V8作为方便之用,所以它们是「隐藏」的。如果对象的描述是相同的,那么隐藏类也会相同。如下图的例子中,对象p和q都属于相同的隐藏类。我们随时可以在JavaScript中新增或删除属性。然而当此事发生时会毁坏归类条件(归纳名称相同的属性)。V8借由建立属性变化所需的新类来解决。属性改变的对象透过一个称为「类型转换(class transition)」的程序纳入新级别中。在类中储存类变换信息当在对象p中加入新属性z时,V8会在Point类内的表格上记录「加入属性z,建立类Point2」。当同一Point类的对象q加入属性z时,V8会先搜寻Point类表。如果它发现了Point2类已加入属性z时,就会将对象q设定在Point2类。内嵌内存正常访问对象属性的过程是:首先获取隐藏类的地址,然后根据属性名查找偏移值,然后计算该属性的地址。虽然相比以往在整个执行环境中查找减小了很大的工作量,但依然比较耗时。能不能将之前查询的结果缓存起来,供再次访问呢?当然是可行的,这就是内嵌缓存。 内嵌缓存的大致思路就是将初次查找的隐藏类和偏移值保存起来,当下次查找的时候,先比较当前对象是否是之前的隐藏类,如果是的话,直接使用之前的缓存结果,减少再次查找表的时间。当然,如果一个对象有多个属性,那么缓存失误的概率就会提高,因为某个属性的类型变化之后,对象的隐藏类也会变化,就与之前的缓存不一致,需要重新使用以前的方式查找哈希表。垃圾回收V8的垃圾回收策略基于分代回收机制,该机制又基于 世代假说。该假说有两个特点:大部分新生对象倾向于早死;不死的对象,会活得更久。在V8中,将内存分为了新生代(new space)和老生代(old space)。它们特点如下:新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。新生代内存回收新生代中的对象主要通过 Scavenge 算法进行垃圾回收。Scavenge 的具体实现,主要采用了Cheney算法。 Cheney算法采用复制的方式进行垃圾回收。它将堆内存一分为二,每一部分空间称为 semispace。这两个空间,只有一个 空间处于使用中,另一个则处于闲置。使用中的 semispace 称为 「From 空间」,闲置的 semispace 称为 「To 空间」。 过程如下: 从 From 空间分配对象,若 semispace 被分配满,则执行 Scavenge 算法进行垃圾回收。 检查 From 空间的存活对象,若对象存活,则检查对象是否符合晋升条件,若符合条件则晋升到老生代,否则将对象从 From 空间复制到 To 空间。 若对象不存活,则释放不存活对象的空间。 完成复制后,将 From 空间与 To 空间进行角色翻转(flip)。Scavenge 算法的缺点是,它的算法机制决定了只能利用一半的内存空间。但是新生代中的对象生存周期短、存活对象少,进行对象复制的成本不是很高,因而非常适合这种场景。老生代内存回收 Mark-Sweep,是标记清除的意思。它主要分为标记和清除两个阶段。 标记阶段,它将遍历堆中所有对象,并对存活的对象进行标记; 清除阶段,对未标记对象的空间进行回收。 与 Scavenge 算法不同,Mark-Sweep 不会对内存一分为二,因此不会浪费空间。但是,经历过一次 Mark-Sweep 之后,内存的空间将会变得不连续,这样会对后续内存分配造成问题。比如,当需要分配一个比较大的对象时,没有任何一个碎片内支持分配,这将提前触发一次垃圾回收,尽管这次垃圾回收是没有必要的。Mark-Compact则是将存活的对象移动到一边,然后再清理端边界外的内存。这篇文章我整理了好久,希望转载表明出处~参考:《WebKit技术内幕》——朱永盛图解浏览器的基本工作原理深入理解V8的垃圾回收原理为什么V8引擎这么快?V8引擎详解V8 Ignition:JS 引擎与字节码的不解之缘 ...

April 9, 2019 · 2 min · jiezi

vscode调试node.js c++扩展

Debugging NodeJS C++ addons using VS Code之前笔者写了一篇 用NAN写一个nodejs的c++扩展, 实际开发过程中,肯定是有单步调试的需求。这里简单介绍用如何用vscode调试node.js c++扩展。一般要调试某个程序,为了能清晰地看到调试的每一行代码、调用的堆栈信息、变量名和函数名等信息,需要待调试程序地 调试符号 信息。比如我们在使用GCC编译程序的时候,如果加上 -g 选项即可在编译后的程序中保留所有的调试符号信息。假如我们有一个hello_world.c的源文件,我们可以通过gcc -g -o hello_world hello_world.c生成一个带调试信息的hello_world程序。类似的,如果我们想要调试node.js扩展,我们也需要扩展源文件的调试符号信息。生成带有调试信息的扩展之前我们通过node-gyp来调用对应的工具来编译项目,想要生成调试符号信息也应该从node-gyp的文档入手,从node-gyp的command options部分可以看到node-gyp支持–debug选项。我们可以通过node-gyp rebuild –debug来生成带有调试信息的node扩展,如果不加–debug表示生成的是一个release扩展。我们在原来项目的package.json文件的scripts部分中增加两个任务,如下:执行npm run rebuild会生成一个build/Release目录。执行npm run rebuild:dev会生成一个build/Debug目录。配置vscodevscode安装lldb插件这里我们将用lldb来调试node扩展。这里我们需要在vscode中安装lldb扩展。安装的过程参考vscode-lldb ,这里不再赘述。配置vscode taskCmd+Shift+P 输入configure task配置一个任务,该任务会执行npm run rebuild:dev,生成带调试信息的node扩展文件。笔者的配置如下:{ “version”: “2.0.0”, “tasks”: [ { “type”: “npm”, “script”: “rebuild:dev”, “problemMatcher”: [] } ]}配置vscode 调试点击debug按钮之后,下面在launch.json中配置调试node扩展的任务,注意在配置的时候增加一个preLaunchTask任务,该任务就是我们上一步配置的。最终luanch.json配置如下:{ “version”: “0.2.0”, “configurations”: [{ “type”: “lldb”, “request”: “launch”, “name”: “Launch Program”, “preLaunchTask”: “npm: build:dev”, “program”: “/absolute/path/to/node”, “args”: [ “/absolute/path/to/your/index.js” ] }]}从launch.json可以看到整个调试的过程为:vscode插件调用lldb,启动nodejs去执行/absolute/path/to/your/index.js,在js文件中会调用node扩展,而该部分扩展已经包含了调试信息,故而可以用于调试。调试node扩展这里为了调试node扩展,我们写了一个demo用于引用Debug版本的node扩展,如下:const addon = require(’../build/Debug/sum’)console.log(addon.sum(1,2))项目地址:https://github.com/warjiang/d…

January 12, 2019 · 1 min · jiezi

memwatch分析

介绍memwatch是一个c++扩展,主要用来观察nodejs内存泄露问题,基本用法如下:const memwatch = require(’@airbnb/memwatch’);function LeakingClass() {}memwatch.gc();var arr = [];var hd = new memwatch.HeapDiff();for (var i = 0; i < 10000; i++) arr.push(new LeakingClass);var hde = hd.end();console.log(JSON.stringify(hde, null, 2));实现分析分析的版本为@airbnb/memwatch。首先从binding.gyp开始入手:{ ’targets’: [ { ’target_name’: ‘memwatch’, ‘include_dirs’: [ “<!(node -e "require(’nan’)")” ], ‘sources’: [ ‘src/heapdiff.cc’, ‘src/init.cc’, ‘src/memwatch.cc’, ‘src/util.cc’ ] } ]}这份配置表示其生成的目标是memwatch.node,源码是src目录下的heapdiff.cc、init.cc、memwatch.cc、util.cc,在项目编译的过程中还需要include额外的nan目录,nan目录通过执行node -e “require(’nan’)按照node模块系统寻找nan依赖,<! 表示后面是一条指令。memwatch的入口函数在init.cc文件中,通过NODE_MODULE(memwatch, init);进行声明。当执行require(’@airbnb/memwatch’)的时候会首先调用init函数:void init (v8::Handle<v8::Object> target){ Nan::HandleScope scope; heapdiff::HeapDiff::Initialize(target); Nan::SetMethod(target, “upon_gc”, memwatch::upon_gc); Nan::SetMethod(target, “gc”, memwatch::trigger_gc); Nan::AddGCPrologueCallback(memwatch::before_gc); Nan::AddGCEpilogueCallback(memwatch::after_gc);}init函数的入口参数v8:Handle<v8:Object> target可以类比nodejs中的module.exports的exports对象。函数内部做的实现可以分为三块,初始化target、给target绑定upon_gc和gc两个函数、在nodejs的gc前后分别挂上对应的钩子函数。Initialize实现到heapdiff.cc文件中来看heapdiff::HeapDiff::Initialize(target);的实现。void heapdiff::HeapDiff::Initialize ( v8::Handle<v8::Object> target ){ Nan::HandleScope scope; v8::Local<v8::FunctionTemplate> t = Nan::New<v8::FunctionTemplate>(New); t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(Nan::New<v8::String>(“HeapDiff”).ToLocalChecked()); Nan::SetPrototypeMethod(t, “end”, End); target->Set(Nan::New<v8::String>(“HeapDiff”).ToLocalChecked(), t->GetFunction());}Initialize函数中创建一个叫做HeapDiff的函数t,同时在t的原型链上绑了end方法,使得js层面可以执行vat hp = new memwatch.HeapDiff();hp.end()。new memwatch.HeapDiff实现当js执行new memwatch.HeapDiff();的时候,c++层面会执行heapdiff::HeapDiff::New函数,去掉注释和不必要的宏,New函数精简如下:NAN_METHOD(heapdiff::HeapDiff::New){ if (!info.IsConstructCall()) { return Nan::ThrowTypeError(“Use the new operator to create instances of this object.”); } Nan::HandleScope scope; HeapDiff * self = new HeapDiff(); self->Wrap(info.This()); s_inProgress = true; s_startTime = time(NULL); self->before = v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(NULL); s_inProgress = false; info.GetReturnValue().Set(info.This());}可以看到用户在js层面执行var hp = new memwatch.HeapDiff();的时候,c++层面会调用nodejs中的v8的api对对堆上内存打一个snapshot保存到self->before中,并将当前对象返回出去。memwatch.HeapDiff.End实现当用户执行hp.end()的时候,会执行原型链上的end方法,也就是c++的heapdiff::HeapDiff::End方法。同样去掉冗余的注释以及宏,End方法可以精简如下:NAN_METHOD(heapdiff::HeapDiff::End){ Nan::HandleScope scope; HeapDiff *t = Unwrap<HeapDiff>( info.This() ); if (t->ended) { return Nan::ThrowError(“attempt to end() a HeapDiff that was already ended”); } t->ended = true; s_inProgress = true; t->after = v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(NULL); s_inProgress = false; v8::Local<Value> comparison = compare(t->before, t->after); ((HeapSnapshot *) t->before)->Delete(); t->before = NULL; ((HeapSnapshot ) t->after)->Delete(); t->after = NULL; info.GetReturnValue().Set(comparison);}在End函数中,拿到当前的HeapDiff对象之后,再对当前的堆上内存再打一个snapshot,调用compare函数对前后两个snapshot对比后得到comparison后,将前后两次snapshot对象释放掉,并将结果通知给js。下面分析下compare函数的具体实现:compare函数内部会递归调用buildIDSet函数得到最终堆快照的diff结果。static v8::Local<Value>compare(const v8::HeapSnapshot * before, const v8::HeapSnapshot * after){ Nan::EscapableHandleScope scope; int s, diffBytes; Local<Object> o = Nan::New<v8::Object>(); // first let’s append summary information Local<Object> b = Nan::New<v8::Object>(); b->Set(Nan::New(“nodes”).ToLocalChecked(), Nan::New(before->GetNodesCount())); //b->Set(Nan::New(“time”), s_startTime); o->Set(Nan::New(“before”).ToLocalChecked(), b); Local<Object> a = Nan::New<v8::Object>(); a->Set(Nan::New(“nodes”).ToLocalChecked(), Nan::New(after->GetNodesCount())); //a->Set(Nan::New(“time”), time(NULL)); o->Set(Nan::New(“after”).ToLocalChecked(), a); // now let’s get allocations by name set<uint64_t> beforeIDs, afterIDs; s = 0; buildIDSet(&beforeIDs, before->GetRoot(), s); b->Set(Nan::New(“size_bytes”).ToLocalChecked(), Nan::New(s)); b->Set(Nan::New(“size”).ToLocalChecked(), Nan::New(mw_util::niceSize(s).c_str()).ToLocalChecked()); diffBytes = s; s = 0; buildIDSet(&afterIDs, after->GetRoot(), s); a->Set(Nan::New(“size_bytes”).ToLocalChecked(), Nan::New(s)); a->Set(Nan::New(“size”).ToLocalChecked(), Nan::New(mw_util::niceSize(s).c_str()).ToLocalChecked()); diffBytes = s - diffBytes; Local<Object> c = Nan::New<v8::Object>(); c->Set(Nan::New(“size_bytes”).ToLocalChecked(), Nan::New(diffBytes)); c->Set(Nan::New(“size”).ToLocalChecked(), Nan::New(mw_util::niceSize(diffBytes).c_str()).ToLocalChecked()); o->Set(Nan::New(“change”).ToLocalChecked(), c); // before - after will reveal nodes released (memory freed) vector<uint64_t> changedIDs; setDiff(beforeIDs, afterIDs, changedIDs); c->Set(Nan::New(“freed_nodes”).ToLocalChecked(), Nan::New<v8::Number>(changedIDs.size())); // here’s where we’ll collect all the summary information changeset changes; // for each of these nodes, let’s aggregate the change information for (unsigned long i = 0; i < changedIDs.size(); i++) { const HeapGraphNode * n = before->GetNodeById(changedIDs[i]); manageChange(changes, n, false); } changedIDs.clear(); // after - before will reveal nodes added (memory allocated) setDiff(afterIDs, beforeIDs, changedIDs); c->Set(Nan::New(“allocated_nodes”).ToLocalChecked(), Nan::New<v8::Number>(changedIDs.size())); for (unsigned long i = 0; i < changedIDs.size(); i++) { const HeapGraphNode * n = after->GetNodeById(changedIDs[i]); manageChange(changes, n, true); } c->Set(Nan::New(“details”).ToLocalChecked(), changesetToObject(changes)); return scope.Escape(o);}该函数中构造了两个对象b(before)、a(after)用于保存前后两个快照的详细信息。用一个js对象描述如下:// b(before) / a(after){ nodes: // heap snapshot中对象节点个数 size_bytes: // heap snapshot的对象大小(bytes) size: // heap snapshot的对象大小(kb、mb) }进一步对前后两次的快照进行分析可以得到o,o中的before、after对象就是前后两次的snapshot对象的引用:// o { before: { // before的堆snapshot nodes: size_bytes: size: }, after: { // after的堆snapshot nodes: size_bytes: size: }, change: { freed_nodes: // gc掉的节点数量 allocated_nodes: // 新增节点数量 details: [ // 按照类型String、Array聚合出来的详细信息 { Array : { what: // 类型 size_bytes: // 字节数bytes size: // kb、mb +: // 新增数量 -: // gc数量 } }, {} ] }}得到两次snapshot对比的结果后将o返回出去,在End函数中通过info.GetReturnValue().Set(comparison);将结果传递到js层面。下面来具体说下compare函数中的buildIDSet、setDiff以及manageChange函数的实现。buildIDSet的用法:buildIDSet(&beforeIDs, before->GetRoot(), s);,该函数会从堆snapshot的根节点出发,递归的寻找所有能够访问的子节点,加入到集合seen中,做DFS统计所有可达节点的同时,也会对所有节点的shallowSize(对象本身占用的内存,不包括引用的对象所占内存)进行累加,统计当前堆所占用的内存大小。其具体实现如下:static void buildIDSet(set<uint64_t> * seen, const HeapGraphNode cur, int & s){ Nan::HandleScope scope; if (seen->find(cur->GetId()) != seen->end()) { return; } if (cur->GetType() == HeapGraphNode::kObject && handleToStr(cur->GetName()).compare(“HeapDiff”) == 0) { return; } s += cur->GetShallowSize(); seen->insert(cur->GetId()); for (int i=0; i < cur->GetChildrenCount(); i++) { buildIDSet(seen, cur->GetChild(i)->GetToNode(), s); }}setDiff函数用法:setDiff(beforeIDs, afterIDs, changedIDs);主要用来计算集合差集用的,具体实现很简单,这里直接贴代码,不再赘述:typedef set<uint64_t> idset;// why doesn’t STL work?// XXX: improve this algorithmvoid setDiff(idset a, idset b, vector<uint64_t> &c){ for (idset::iterator i = a.begin(); i != a.end(); i++) { if (b.find(*i) == b.end()) c.push_back(*i); }}manageChange函数用法:manageChange(changes, n, false);,其作用在于做数据的聚合。对某个指定的set,按照set中对象的类型,聚合出每种对象创建了多少、销毁了多少,实现如下:static void manageChange(changeset & changes, const HeapGraphNode * node, bool added){ std::string type; switch(node->GetType()) { case HeapGraphNode::kArray: type.append(“Array”); break; case HeapGraphNode::kString: type.append(“String”); break; case HeapGraphNode::kObject: type.append(handleToStr(node->GetName())); break; case HeapGraphNode::kCode: type.append(“Code”); break; case HeapGraphNode::kClosure: type.append(“Closure”); break; case HeapGraphNode::kRegExp: type.append(“RegExp”); break; case HeapGraphNode::kHeapNumber: type.append(“Number”); break; case HeapGraphNode::kNative: type.append(“Native”); break; case HeapGraphNode::kHidden: default: return; } if (changes.find(type) == changes.end()) { changes[type] = change(); } changeset::iterator i = changes.find(type); i->second.size += node->GetShallowSize() * (added ? 1 : -1); if (added) i->second.added++; else i->second.released++; return;}upon_gc和gc实现这两个方法的在init函数中声明如下:Nan::SetMethod(target, “upon_gc”, memwatch::upon_gc);Nan::SetMethod(target, “gc”, memwatch::trigger_gc);先看gc方法的实现,实际上对应memwatch::trigger_gc,实现如下:NAN_METHOD(memwatch::trigger_gc) { Nan::HandleScope scope; int deadline_in_ms = 500; if (info.Length() >= 1 && info[0]->IsNumber()) { deadline_in_ms = (int)(info[0]->Int32Value()); } Nan::IdleNotification(deadline_in_ms); Nan::LowMemoryNotification(); info.GetReturnValue().Set(Nan::Undefined());}通过Nan::IdleNotification和Nan::LowMemoryNotification触发v8的gc功能。再来看upon_gc方法,该方法实际上会绑定一个函数,当执行到gc方法时,就会触发该函数:NAN_METHOD(memwatch::upon_gc) { Nan::HandleScope scope; if (info.Length() >= 1 && info[0]->IsFunction()) { uponGCCallback = new UponGCCallback(info[0].As<v8::Function>()); } info.GetReturnValue().Set(Nan::Undefined());}其中info[0]就是用户传入的回调函数。调用new UponGCCallback的时候,其对应的构造函数内部会执行:UponGCCallback(v8::Local<v8::Function> callback_) : Nan::AsyncResource(“memwatch:upon_gc”) { callback.Reset(callback_);}把用户传入的callback_函数设置到UponGCCallback类的成员变量callback上。upon_gc回调的触发与gc的钩子有关,详细看下一节分析。gc前、后钩子函数的实现gc钩子的挂载如下:Nan::AddGCPrologueCallback(memwatch::before_gc);Nan::AddGCEpilogueCallback(memwatch::after_gc);先来看memwatch::before_gc函数的实现,内部给gc开始记录了时间:NAN_GC_CALLBACK(memwatch::before_gc) { currentGCStartTime = uv_hrtime();}再来看memwatch::after_gc函数的实现,内部会在gc后记录gc的结果到GCStats结构体中:struct GCStats { // counts of different types of gc events size_t gcScavengeCount; // gc 扫描次数 uint64_t gcScavengeTime; // gc 扫描事件 size_t gcMarkSweepCompactCount; // gc标记清除整理的个数 uint64_t gcMarkSweepCompactTime; // gc标记清除整理的时间 size_t gcIncrementalMarkingCount; // gc增量标记的个数 uint64_t gcIncrementalMarkingTime; // gc增量标记的时间 size_t gcProcessWeakCallbacksCount; // gc处理weakcallback的个数 uint64_t gcProcessWeakCallbacksTime; // gc处理weakcallback的时间};对gc请求进行统计后,通过v8的api获取堆的使用情况,最终将结果保存到barton中,barton内部维护了一个uv_work_t的变量req,req的data字段指向barton对象本身。NAN_GC_CALLBACK(memwatch::after_gc) { if (heapdiff::HeapDiff::InProgress()) return; uint64_t gcEnd = uv_hrtime(); uint64_t gcTime = gcEnd - currentGCStartTime; switch(type) { case kGCTypeScavenge: s_stats.gcScavengeCount++; s_stats.gcScavengeTime += gcTime; return; case kGCTypeMarkSweepCompact: case kGCTypeAll: break; } if (type == kGCTypeMarkSweepCompact) { s_stats.gcMarkSweepCompactCount++; s_stats.gcMarkSweepCompactTime += gcTime; Nan::HandleScope scope; Baton * baton = new Baton; v8::HeapStatistics hs; Nan::GetHeapStatistics(&hs); timeval tv; gettimeofday(&tv, NULL); baton->gc_ts = (tv.tv_sec * 1000000) + tv.tv_usec; baton->total_heap_size = hs.total_heap_size(); baton->total_heap_size_executable = hs.total_heap_size_executable(); baton->req.data = (void ) baton; uv_queue_work(uv_default_loop(), &(baton->req), noop_work_func, (uv_after_work_cb)AsyncMemwatchAfter); }}在前面工作完成的基础上,将结果丢到libuv的loop中,等到合适的实际触发回调函数,在回调函数中可以拿到req对象,通过访问req.data对其做强制类型装换可以得到barton对象,在loop的回调函数中,将barton中封装的数据依次取出来,保存到stats对象中,并调用uponGCCallback的Call方法,传入字面量stats和stats对象。static void AsyncMemwatchAfter(uv_work_t request) { Nan::HandleScope scope; Baton * b = (Baton *) request->data; // if there are any listeners, it’s time to emit! if (uponGCCallback) { Local<Value> argv[2]; Local<Object> stats = Nan::New<v8::Object>(); stats->Set(Nan::New(“gc_ts”).ToLocalChecked(), javascriptNumber(b->gc_ts)); stats->Set(Nan::New(“gcProcessWeakCallbacksCount”).ToLocalChecked(), javascriptNumberSize(b->stats.gcProcessWeakCallbacksCount)); stats->Set(Nan::New(“gcProcessWeakCallbacksTime”).ToLocalChecked(), javascriptNumber(b->stats.gcProcessWeakCallbacksTime)); stats->Set(Nan::New(“peak_malloced_memory”).ToLocalChecked(), javascriptNumberSize(b->peak_malloced_memory)); stats->Set(Nan::New(“gc_time”).ToLocalChecked(), javascriptNumber(b->gc_time)); // the type of event to emit argv[0] = Nan::New(“stats”).ToLocalChecked(); argv[1] = stats; uponGCCallback->Call(2, argv); } delete b;}最后在Call函数的内部调用js传入的callback_函数,并将字面量stats和stats对象传递到js层面,供上层用户使用。void Call(int argc, Local<v8::Value> argv[]) { v8::Isolate *isolate = v8::Isolate::GetCurrent(); runInAsyncScope(isolate->GetCurrentContext()->Global(), Nan::New(callback), argc, argv);} ...

January 8, 2019 · 5 min · jiezi

用NAN写一个nodejs的c++扩展

NAN介绍NAN的全称为Native Abstraction for Node.js, 其表现上是一个Node.js包。安装后,就得到一堆C++头文件,里面是一堆宏。它主要为Node.js和V8跨版本提供了封装的宏,使得开发者不用关心各个版本之间的API的差异。(from 《nodejs来一打C++扩展》)NAN的优势在于可以屏蔽不同版本Node的API,使得C++扩展可以wirte once, compile anywhere,一份C++扩展可以适用于不同版本的Node.js。这里的c++扩展实现的功能是一个求和的扩展(hello world太多了,写个不一样的)扩展地址为:https://www.npmjs.com/package…,项目代码地址:https://github.com/warjiang/d…使用方式如下:项目目录如下:在开发之前我们首先需要安装nan包(npm install nan -S)。扩展开发分成两个层面,c++层面和JS层面。src目录中主要是c++代码,也是扩展的实现部分。index.js引用c++扩展,暴露出方法供上层使用。初次开发nodejs扩展的用户需要注意下项目目录中的binding.gyp文件(node-gyp会读取项目中的binding.gyp):target_name为sum,表示最后生成的扩展文件名为sum.node。include_dirs表示除了nodejs基础的依赖之外,我们还需要nan的头文件,<!(node -e "require(’nan’)") 中<!表示后面是命令,node -e “require(’nan’)“就是利用nodejs的require能力,寻找nan的目录,执行效果如下:sources项指明了c++扩展需要编译的源文件。c++部分开发先直接上代码(src/init.cc):#include <v8.h>#include <node.h>#include <nan.h>using v8::Local;using v8::Object;using v8::Number;NAN_METHOD(sum){ Nan::HandleScope scope; uint32_t sum = 0; for(int i = 0; i< info.Length(); i++){ sum += info[i]->NumberValue(); } info.GetReturnValue().Set(Nan::New(sum));}void init (Local<Object> exports){ Nan::HandleScope scope; Nan::SetMethod(exports, “sum”, sum);}NODE_MODULE(memwatch, init);扩展的入口从NODE_MODULE(memwatch, init);开始,当js层面执行了require(‘path/to/xxx.node’)的时候,就会执行init函数。init函数的入参可以类比module.exports对象,这里我们给exports对象增加了一个名为sum的方法,其对应的实现为NAN_METHOD(sum)部分。NAN_METHOD(sum)通过宏定义对sum函数进行包装,sum函数的入参为info数组,我们再这里遍历info数组,通过info[i]->NumberValue方法将每个入参对应的number类型的值取出来,加到sum中去。累加完成后通过info.GetReturnValue().Set(Nan::New(sum))将sum结果返回出去。这样其实我们的c++部分扩展就已经开发完毕了,可以通过执行node-gyp configure && node-gyp build编译项目,在build/Release目录下会生成sum.node的文件。我们可以启动一个node的命令行进行验证:// node cli> let addon = require(’./build/Release/sum’)> addon.sum(1) // 1> addon.sum(1,2) // 3引用build/Release/sum的方式实际开发中十分不方便,我们可以用js对这行代码进行封装,在js内部引用build/Release/sum,暴露出来方法给外部进行调用。js部分开发有了上面的铺垫,这里我们开发js部分就显得十分自然。直接上代码const addon = require(’./build/Release/sum’)module.exports = addon.sum一共就两行代码,逻辑清晰简单,就引用编译好的扩展,将sum方法暴露出去。发布nodejs扩展发布的时候需要在package.json的scripts部分增加install钩子的处理,如下:用户安装扩展的时候,会在install的钩子上,帮助用户执行node-gyp rebuild来在用户的机器上生成对应的扩展文件。这样我们的开发就完毕了,执行npm publish将npm包发布出去 ...

January 8, 2019 · 1 min · jiezi

JavaScript是如何工作的:深入V8引擎&编写优化代码的5个技巧

本系列的 第一篇文章 主要介绍引擎、运行时和调用堆栈。第二篇文章将深入谷歌 V8 的JavaScript引擎的内部。概述JavaScript引擎是执行 JavaScript 代码的程序或解释器。JavaScript引擎可以实现为标准解释器,或者以某种形式将JavaScript编译为字节码的即时编译器。以为实现JavaScript引擎的流行项目的列表:V8 — 开源,由 Google 开发,用 C ++ 编写Rhino — 由 Mozilla 基金会管理,开源,完全用 Java 开发SpiderMonkey — 是第一个支持 Netscape Navigator 的 JavaScript 引擎,目前正供 Firefox 使用JavaScriptCore — 开源,以Nitro形式销售,由苹果为Safari开发KJS — KDE 的引擎,最初由 Harri Porten 为 KDE 项目中的 Konqueror 网页浏览器开发Chakra (JScript9) — Internet ExplorerChakra (JavaScript) — Microsoft EdgeNashorn, 作为 OpenJDK 的一部分,由 Oracle Java 语言和工具组编写JerryScript — 物联网的轻量级引擎为什么要创建V8引擎?由谷歌构建的V8引擎是开源的,使用c++编写。这个引擎是在谷歌Chrome中使用的,但是,与其他引擎不同的是 V8 也用于流行的 node.js。V8最初被设计用来提高web浏览器中JavaScript执行的性能。为了获得速度,V8 将 JavaScript 代码转换成更高效的机器码,而不是使用解释器。它通过实现 JIT (Just-In-Time) 编译器将 JavaScript 代码编译为执行时的机器码,就像许多现代 JavaScript 引擎(如SpiderMonkey或Rhino (Mozilla)) 所做的那样。这里的主要区别是 V8 不生成字节码或任何中间代码。V8 曾有两个编译器在 V8 的 5.9 版本出来之前,V8 引擎使用了两个编译器:full-codegen — 一个简单和非常快的编译器,产生简单和相对较慢的机器码。Crankshaft — 一种更复杂(Just-In-Time)的优化编译器,生成高度优化的代码。V8 引擎也在内部使用多个线程:主线程执行你所期望的操作:获取代码、编译代码并执行它还有一个单独的线程用于编译,因此主线程可以在前者优化代码的同时继续执行一个 Profiler 线程,它会告诉运行时我们花了很多时间,让 Crankshaft 可以优化它们一些线程处理垃圾收集器当第一次执行 JavaScript 代码时,V8 利用 full-codegen 编译器,直接将解析的 JavaScript 翻译成机器代码而不进行任何转换。这使得它可以非常快速地开始执行机器代码。请注意,V8 不使用中间字节码,从而不需要解释器。当代码已经运行一段时间后,分析线程已经收集了足够的数据来判断应该优化哪个方法。接下来,Crankshaft 从另一个线程开始优化。它将 JavaScript 抽象语法树转换为被称为 Hydrogen 的高级静态单分配(SSA)表示,并尝试优化 Hydrogen 图,大多数优化都是在这个级别完成的。内联代码第一个优化是提前内联尽可能多的代码。内联是用被调用函数的主体替换调用点(调用函数的代码行)的过程。这个简单的步骤允许下面的优化更有意义。隐藏类JavaScript是一种基于原型的语言:没有使用克隆过程创建类和对象。JavaScript也是一种动态编程语言,这意味着可以在实例化后轻松地在对象中添加或删除属性。大多数 JavaScript 解释器使用类似字典的结构(基于哈希函数)来存储对象属性值在内存中的位置,这种结构使得在 JavaScript 中检索属性的值比在 Java 或 C# 等非动态编程语言中的计算成本更高。在Java中,所有对象属性都是在编译之前由固定对象布局确定的,并且无法在运行时动态添加或删除(当然,C#具有动态类型,这是另一个主题)。 因此,属性值(或指向这些属性的指针)可以作为连续缓冲区存储在存储器中,每个缓冲区之间具有固定偏移量, 可以根据属性类型轻松确定偏移的长度,而在运行时可以更改属性类型的 JavaScript 中这是不可能的。由于使用字典查找内存中对象属性的位置效率非常低,因此 V8 使用了不同的方法:隐藏类。隐藏类与 Java 等语言中使用的固定对象(类)的工作方式类似,只是它们是在运行时创建的。现在,让我们看看他们实际的例子:一旦 “new Point(1,2)” 调用发生,V8 将创建一个名为 “C0” 的隐藏类。尚未为 Point 定义属性,因此“C0”为空。一旦第一个语句“this.x = x”被执行(在“Point”函数内),V8 将创建一个名为 “C1” 的第二个隐藏类,它基于“C0”。 “C1”描述了可以找到属性 x 的存储器中的位置(相对于对象指针)。 在这种情况下,“x”存储在偏移0处,这意味着当将存储器中的 point 对象视为连续缓冲区时,第一偏移将对应于属性 “x”。 V8 还将使用 “类转换” 更新 “C0” ,该类转换指出如果将属性 “x” 添加到 point 对象,则隐藏类应从 “C0” 切换到 “C1”。 下面的 point 对象的隐藏类现在是“C1”。每次将新属性添加到对象时,旧的隐藏类都会更新为指向新隐藏类的转换路径。隐藏类转换非常重要,因为它们允许在以相同方式创建的对象之间共享隐藏类。如果两个对象共享一个隐藏类并且同一属性被添加到它们中,则转换将确保两个对象都接收相同的新隐藏类以及随其附带的所有优化代码。当语句 “this.y = y” 被执行时,会重复同样的过程(在 “Point” 函数内部,“this.x = x”语句之后)。一个名为“C2”的新隐藏类会被创建,如果将一个属性 “y” 添加到一个 Point 对象(已经包含属性“x”),一个类转换会添加到“C1”,则隐藏类应该更改为“C2”,point 对象的隐藏类更新为“C2”。隐藏类转换取决于将属性添加到对象的顺序。看看下面的代码片段:现在,假设对于p1和p2,将使用相同的隐藏类和转换。那么,对于“p1”,首先添加属性“a”,然后添加属性“b”。然而,“p2”首先分配“b”,然后是“a”。因此,由于不同的转换路径,“p1”和“p2”以不同的隐藏类别结束。在这种情况下,以相同的顺序初始化动态属性好得多,以便隐藏的类可以被重用。内联缓存V8利用了另一种优化动态类型语言的技术,称为内联缓存。内联缓存依赖于这样一种观察,即对同一方法的重复调用往往发生在同一类型的对象上。这里可以找到对内联缓存的深入解释。接下来将讨论内联缓存的一般概念(如果您没有时间通过上面的深入了解)。那么它是如何工作的呢? V8 维护了在最近的方法调用中作为参数传递的对象类型的缓存,并使用这些信息预测将来作为参数传递的对象类型。如果 V8 能够很好地预测传递给方法的对象的类型,它就可以绕过如何访问对象属性的过程,而是使用从以前的查找到对象的隐藏类的存储信息。那么隐藏类和内联缓存的概念如何相关呢?无论何时在特定对象上调用方法时,V8 引擎都必须执行对该对象的隐藏类的查找,以确定访问特定属性的偏移量。在同一个隐藏类的两次成功的调用之后,V8 省略了隐藏类的查找,并简单地将该属性的偏移量添加到对象指针本身。对于该方法的所有下一次调用,V8 引擎都假定隐藏的类没有更改,并使用从以前的查找存储的偏移量直接跳转到特定属性的内存地址。这大大提高了执行速度。内联缓存也是为什么相同类型的对象共享隐藏类非常重要的原因。 如果你创建两个相同类型和不同隐藏类的对象(正如我们之前的例子中所做的那样),V8将无法使用内联缓存,因为即使这两个对象属于同一类型,它们对应的隐藏类为其属性分配不同的偏移量。这两个对象基本相同,但是“a”和“b”属性的创建顺序不同。编译成机器码一旦 Hydrogen 图被优化,Crankshaft 将其降低到称为 Lithium 的较低级表示。大部分的 Lithium 实现都是特定于架构的。寄存器分配往往发生在这个级别。最后,Lithium 被编译成机器码。然后就是 OSR :on-stack replacement(堆栈替换)。在我们开始编译和优化一个明确的长期运行的方法之前,我们可能会运行堆栈替换。 V8 不只是缓慢执行堆栈替换,并再次开始优化。相反,它会转换我们拥有的所有上下文(堆栈,寄存器),以便在执行过程中切换到优化版本上。这是一个非常复杂的任务,考虑到除了其他优化之外,V8 最初还将代码内联。 V8 不是唯一能够做到的引擎。有一种叫去优化的安全措施来进行相反的转换,并在假设引擎无效的情况下返回未优化的代码。垃圾收集对于垃圾收集,V8采用传统的 mark-and-sweep 算法 来清理旧一代。 标记阶段应该停止JavaScript执行。 为了控制GC成本并使执行更稳定,V8使用增量标记:不是遍历整个堆,尝试标记每个可能的对象,它只是遍历堆的一部分,然后恢复正常执行。下一个GC停止将从上一个堆行走停止的位置继续,这允许在正常执行期间非常短暂的暂停,如前所述,扫描阶段由单独的线程处理。如何编写优化的 JavaScript对象属性的顺序:始终以相同的顺序实例化对象属性,以便可以共享隐藏的类和随后优化的代码。动态属性: 因为在实例化之后向对象添加属性将强制执行隐藏的类更改,并降低之前隐藏类所优化的所有方法的执行速度,所以在其构造函数中分配所有对象的属性。方法:重复执行相同方法的代码将比仅执行一次的多个不同方法(由于内联缓存)的代码运行得更快。数组:避免稀疏数组,其中键值不是自增的数字,并没有存储所有元素的稀疏数组是哈希表。这种数组中的元素访问开销较高。另外,尽量避免预分配大数组。最好是按需增长。最后,不要删除数组中的元素,这会使键值变得稀疏。标记值:V8 使用 32 位表示对象和数值。由于数值是 31 位的,它使用了一位来区分它是一个对象(flag = 1)还是一个称为 SMI(SMall Integer)整数(flag = 0)。那么,如果一个数值大于 31 位,V8会将该数字装箱,把它变成一个双精度数,并创建一个新的对象来存放该数字。尽可能使用 31 位有符号数字,以避免对 JS 对象的高开销的装箱操作。Ignition and TurboFan随着2017年早些时候发布V8 5.9,引入了新的执行管道。 这个新的管道在实际的JavaScript应用程序中实现了更大的性能提升和显着节省内存。新的执行流程是建立在 Ignition( V8 的解释器)和 TurboFan( V8 的最新优化编译器)之上的。自从 V8 5.9 版本问世以来,由于 V8 团队一直努力跟上新的 JavaScript 语言特性以及这些特性所需要的优化,V8 团队已经不再使用 full-codegen 和 Crankshaft(自 2010 年以来为 V8 技术所服务)。这意味着 V8 整体上将有更简单和更易维护的架构。这些改进只是一个开始。 新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提升JavaScript性能并缩小V8在Chrome和Node.js中的占用空间。原文:https://blog.sessionstack.com…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

December 13, 2018 · 2 min · jiezi

「译」更快的 async 函数和 promises

翻译自:Faster async functions and promisesJavaScript 的异步过程一直被认为是不够快的,更糟糕的是,在 NodeJS 等实时性要求高的场景下调试堪比噩梦。不过,这一切正在改变,这篇文章会详细解释我们是如何优化 V8 引擎(也会涉及一些其它引擎)里的 async 函数和 promises 的,以及伴随着的开发体验的优化。温馨提示: 这里有个 视频,你可以结合着文章看。异步编程的新方案从 callbacks 到 promises,再到 async 函数在 promises 正式成为 JavaScript 标准的一部分之前,回调被大量用在异步编程中,下面是个例子:function handler(done) { validateParams((error) => { if (error) return done(error); dbQuery((error, dbResults) => { if (error) return done(error); serviceCall(dbResults, (error, serviceResults) => { console.log(result); done(error, serviceResults); }); }); });}类似以上深度嵌套的回调通常被称为「回调黑洞」,因为它让代码可读性变差且不易维护。幸运地是,现在 promises 成为了 JavaScript 语言的一部分,以下实现了跟上面同样的功能:function handler() { return validateParams() .then(dbQuery) .then(serviceCall) .then(result => { console.log(result); return result; });}最近,JavaScript 支持了 async 函数,上面的异步代码可以写成像下面这样的同步的代码:async function handler() { await validateParams(); const dbResults = await dbQuery(); const results = await serviceCall(dbResults); console.log(results); return results;}借助 async 函数,代码变得更简洁,代码的逻辑和数据流都变得更可控,当然其实底层实现还是异步。(注意,JavaScript 还是单线程执行,async 函数并不会开新的线程。)从事件监听回调到 async 迭代器NodeJS 里 ReadableStreams 作为另一种形式的异步也特别常见,下面是个例子:const http = require(‘http’);http.createServer((req, res) => { let body = ‘’; req.setEncoding(‘utf8’); req.on(‘data’, (chunk) => { body += chunk; }); req.on(’end’, () => { res.write(body); res.end(); });}).listen(1337);这段代码有一点难理解:只能通过回调去拿 chunks 里的数据流,而且数据流的结束也必须在回调里处理。如果你没能理解到函数是立即结束但实际处理必须在回调里进行,可能就会引入 bug。同样很幸运,ES2018 特性里引入的一个很酷的 async 迭代器 可以简化上面的代码:const http = require(‘http’);http.createServer(async (req, res) => { try { let body = ‘’; req.setEncoding(‘utf8’); for await (const chunk of req) { body += chunk; } res.write(body); res.end(); } catch { res.statusCode = 500; res.end(); }}).listen(1337);你可以把所有数据处理逻辑都放到一个 async 函数里使用 for await…of 去迭代 chunks,而不是分别在 ‘data’ 和 ’end’ 回调里处理,而且我们还加了 try-catch 块来避免 unhandledRejection 问题。以上这些特性你今天就可以在生成环境使用!async 函数从 Node.js 8 (V8 v6.2 / Chrome 62) 开始就已全面支持,async 迭代器从 Node.js 10 (V8 v6.8 / Chrome 68) 开始支持。async 性能优化从 V8 v5.5 (Chrome 55 & Node.js 7) 到 V8 v6.8 (Chrome 68 & Node.js 10),我们致力于异步代码的性能优化,目前的效果还不错,你可以放心地使用这些新特性。上面的是 doxbee 基准测试,用于反应重度使用 promise 的性能,图中纵坐标表示执行时间,所以越小越好。另一方面,parallel 基准测试 反应的是重度使用 Promise.all() 的性能情况,结果如下:Promise.all 的性能提高了八倍!然后,上面的测试仅仅是小的 DEMO 级别的测试,V8 团队更关心的是 实际用户代码的优化效果。上面是基于市场上流行的 HTTP 框架做的测试,这些框架大量使用了 promises 和 async 函数,这个表展示的是每秒请求数,所以跟之前的表不一样,这个是数值越大越好。从表可以看出,从 Node.js 7 (V8 v5.5) 到 Node.js 10 (V8 v6.8) 性能提升了不少。性能提升取决于以下三个因素:TurboFan,新的优化编译器 ????Orinoco,新的垃圾回收器 ????一个 Node.js 8 的 bug 导致 await 跳过了一些微 tick(microticks) ????当我们在 Node.js 8 里 启用 TurboFan 的后,性能得到了巨大的提升。同时我们引入了一个新的垃圾回收器,叫作 Orinoco,它把垃圾回收从主线程中移走,因此对请求响应速度提升有很大帮助。最后,Node.js 8 中引入了一个 bug 在某些时候会让 await 跳过一些微 tick,这反而让性能变好了。这个 bug 是因为无意中违反了规范导致的,但是却给了我们优化的一些思路。这里我们稍微解释下:const p = Promise.resolve();(async () => { await p; console.log(‘after:await’);})();p.then(() => console.log(’tick:a’)) .then(() => console.log(’tick:b’));上面代码一开始创建了一个已经完成状态的 promise p,然后 await 出其结果,又同时链了两个 then,那最终的 console.log 打印的结果会是什么呢?因为 p 是已完成的,你可能认为其会先打印 ‘after:await’,然后是剩下两个 tick, 事实上 Node.js 8 里的结果是:虽然以上结果符合预期,但是却不符合规范。Node.js 10 纠正了这个行为,会先执行 then 链里的,然后才是 async 函数。这个「正确的行为」看起来并不正常,甚至会让很多 JavaScript 开发者感到吃惊,还是有必要再详细解释下。在解释之前,我们先从一些基础开始。任务(tasks)vs. 微任务(microtasks)从某层面上来说,JavaScript 里存在任务和微任务。任务处理 I/O 和计时器等事件,一次只处理一个。微任务是为了 async/await 和 promise 的延迟执行设计的,每次任务最后执行。在返回事件循环(event loop)前,微任务的队列会被清空。可以通过 Jake Archibald 的 tasks, microtasks, queues, and schedules in the browser 了解更多。Node.js 里任务模型与此非常类似。async 函数根据 MDN,async 函数是一个通过异步执行并隐式返回 promise 作为结果的函数。从开发者角度看,async 函数让异步代码看起来像同步代码。一个最简单的 async 函数:async function computeAnswer() { return 42;}函数执行后会返回一个 promise,你可以像使用其它 promise 一样用其返回的值。const p = computeAnswer();// → Promisep.then(console.log);// prints 42 on the next turn你只能在下一个微任务执行后才能得到 promise p 返回的值,换句话说,上面的代码语义上等价于使用 Promise.resolve 得到的结果:function computeAnswer() { return Promise.resolve(42);}async 函数真正强大的地方来源于 await 表达式,它可以让一个函数执行暂停直到一个 promise 已接受(resolved),然后等到已完成(fulfilled)后恢复执行。已完成的 promise 会作为 await 的值。这里的例子会解释这个行为:async function fetchStatus(url) { const response = await fetch(url); return response.status;}fetchStatus 在遇到 await 时会暂停,当 fetch 这个 promise 已完成后会恢复执行,这跟直接链式处理 fetch 返回的 promise 某种程度上等价。function fetchStatus(url) { return fetch(url).then(response => response.status);}链式处理函数里包含了之前跟在 await 后面的代码。 正常来说你应该在 await 后面放一个 Promise,不过其实后面可以跟任意 JavaScript 的值,如果跟的不是 promise,会被制转为 promise,所以 await 42 效果如下:async function foo() { const v = await 42; return v;}const p = foo();// → Promisep.then(console.log);// prints 42 eventually更有趣的是,await 后可以跟任何 “thenable”,例如任何含有 then 方法的对象,就算不是 promise 都可以。因此你可以实现一个有意思的 类来记录执行时间的消耗:class Sleep { constructor(timeout) { this.timeout = timeout; } then(resolve, reject) { const startTime = Date.now(); setTimeout(() => resolve(Date.now() - startTime), this.timeout); }}(async () => { const actualTime = await new Sleep(1000); console.log(actualTime);})();一起来看看 V8 规范 里是如何处理 await 的。下面是很简单的 async 函数 foo:async function foo(v) { const w = await v; return w;}执行时,它把参数 v 封装成一个 promise,然后会暂停直到 promise 完成,然后 w 赋值为已完成的 promise,最后 async 返回了这个值。神秘的 await首先,V8 会把这个函数标记为可恢复的,意味着执行可以被暂停并恢复(从 await 角度看是这样的)。然后,会创建一个所谓的 implicit_promise(用于把 async 函数里产生的值转为 promise)。然后是有意思的东西来了:真正的 await。首先,跟在 await 后面的值被转为 promise。然后,处理函数会绑定这个 promise 用于在 promise 完成后恢复主函数,此时 async 函数被暂停了,返回 implicit_promise 给调用者。一旦 promise 完成了,函数会恢复并拿到从 promise 得到值 w,最后,implicit_promise 会用 w 标记为已接受。简单说,await v 初始化步骤有以下组成:把 v 转成一个 promise(跟在 await 后面的)。绑定处理函数用于后期恢复。暂停 async 函数并返回 implicit_promise 给掉用者。我们一步步来看,假设 await 后是一个 promise,且最终已完成状态的值是 42。然后,引擎会创建一个新的 promise 并且把 await 后的值作为 resolve 的值。借助标准里的 PromiseResolveThenableJob 这些 promise 会被放到下个周期执行。然后,引擎创建了另一个叫做 throwaway 的 promise。之所以叫这个名字,因为没有其它东西链过它,仅仅是引擎内部用的。throwaway promise 会链到含有恢复处理函数的 promise 上。这里 performPromiseThen 操作其实内部就是 Promise.prototype.then()。最终,该 async 函数会暂停,并把控制权交给调用者。调用者会继续执行,最终调用栈会清空,然后引擎会开始执行微任务:运行之前已准备就绪的 PromiseResolveThenableJob,首先是一个 PromiseReactionJob,它的工作仅仅是在传递给 await 的值上封装一层 promise。然后,引擎回到微任务队列,因为在回到事件循环之前微任务队列必须要清空。然后是另一个 PromiseReactionJob,等待我们正在 await(我们这里指的是 42)这个 promise 完成,然后把这个动作安排到 throwaway promise 里。引擎继续回到微任务队列,因为还有最后一个微任务。现在这第二个 PromiseReactionJob 把决定传达给 throwaway promise,并恢复 async 函数的执行,最后返回从 await 得到的 42。总结下,对于每一个 await 引擎都会创建两个额外的 promise(即使右值已经是一个 promise),并且需要至少三个微任务。谁会想到一个简单的 await 竟然会有如此多冗余的运算?!我们来看看到底是什么引起冗余。第一行的作用是封装一个 promise,第二行为了 resolve 封装后的 promose await 之后的值 v。这两行产生个冗余的 promise 和两个冗余的微任务。如果 v 已经是 promise 的话就很不划算了(大多时候确实也是如此)。在某些特殊场景 await 了 42 的话,那确实还是需要封装成 promise 的。因此,这里可以使用 promiseResolve 操作来处理,只有必要的时候才会进行 promise 的封装:如果入参是 promise,则原封不动地返回,只封装必要的 promise。这个操作在值已经是 promose 的情况下可以省去一个额外的 promise 和两个微任务。此特性可以通过 –harmony-await-optimization 参数在 V8(从 v7.1 开始)中开启,同时我们 向 ECMAScript 发起了一个提案,目测很快会合并。下面是简化后的 await 执行过程:感谢神奇的 promiseResolve,现在我们只需要传 v 即可而不用关心它是什么。之后跟之前一样,引擎会创建一个 throwaway promise 并放到 PromiseReactionJob 里为了在下一个 tick 时恢复该 async 函数,它会先暂停函数,把自身返回给掉用者。当最后所有执行完毕,引擎会跑微任务队列,会执行 PromiseReactionJob。这个任务会传递 promise 结果给 throwaway,并且恢复 async 函数,从 await 拿到 42。尽管是内部使用,引擎创建 throwaway promise 可能还是会让人觉得哪里不对。事实证明,throwaway promise 仅仅是为了满足规范里 performPromiseThen 的需要。这是最近提议给 ECMAScript 的 变更,引擎大多数时候不再需要创建 throwaway 了。对比 await 在 Node.js 10 和优化后(应该会放到 Node.js 12 上)的表现:async/await 性能超过了手写的 promise 代码。关键就是我们减少了 async 函数里一些不必要的开销,不仅仅是 V8 引擎,其它 JavaScript 引擎都通过这个 补丁 实现了优化。开发体验优化除了性能,JavaScript 开发者也很关心问题定位和修复,这在异步代码里一直不是件容易的事。Chrome DevTools 现在支持了异步栈追踪:在本地开发时这是个很有用的特性,不过一旦应用部署了就没啥用了。调试时,你只能看到日志文件里的 Error#stack 信息,这些并不会包含任何异步信息。最近我们搞的 零成本异步栈追踪 使得 Error#stack 包含了 async 函数的调用信息。「零成本」听起来很让人兴奋,对吧?当 Chrome DevTools 功能带来重大开销时,它如何才能实现零成本?举个例子,foo 里调用 bar,bar 在 await 一个 promise 后抛一个异常:async function foo() { await bar(); return 42;}async function bar() { await Promise.resolve(); throw new Error(‘BEEP BEEP’);}foo().catch(error => console.log(error.stack));这段代码在 Node.js 8 或 Node.js 10 运行结果如下:$ node index.jsError: BEEP BEEP at bar (index.js:8:9) at process._tickCallback (internal/process/next_tick.js:68:7) at Function.Module.runMain (internal/modules/cjs/loader.js:745:11) at startup (internal/bootstrap/node.js:266:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)注意到,尽管是 foo() 里的调用抛的错,foo 本身却不在栈追踪信息里。如果应用是部署在云容器里,这会让开发者很难去定位问题。有意思的是,引擎是知道 bar 结束后应该继续执行什么的:即 foo 函数里 await 后。恰好,这里也正是 foo 暂停的地方。引擎可以利用这些信息重建异步的栈追踪信息。有了以上优化,输出就会变成这样:$ node –async-stack-traces index.jsError: BEEP BEEP at bar (index.js:8:9) at process._tickCallback (internal/process/next_tick.js:68:7) at Function.Module.runMain (internal/modules/cjs/loader.js:745:11) at startup (internal/bootstrap/node.js:266:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3) at async foo (index.js:2:3)在栈追踪信息里,最上层的函数出现在第一个,之后是一些异步调用栈,再后面是 foo 里面 bar 上下文的栈信息。这个特性的启用可以通过 V8 的 –async-stack-traces 参数启用。然而,如果你跟上面 Chrome DevTools 里的栈信息对比,你会发现栈追踪里异步部分缺失了 foo 的调用点信息。这里利用了 await 恢复和暂停位置是一样的特性,但 Promise#then() 或 Promise#catch() 就不是这样的。可以看 Mathias Bynens 的文章 await beats Promise#then() 了解更多。结论async 函数变快少不了以下两个优化:移除了额外的两个微任务移除了 throwaway promise除此之外,我们通过 零成本异步栈追踪 提升了 await 和 Promise.all() 开发调试体验。我们还有些对 JavaScript 开发者友好的性能建议:多使用 async 和 await 而不是手写 promise 代码,多使用 JavaScript 引擎提供的 promise 而不是自己去实现。文章可随意转载,但请保留此 原文链接。非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。 ...

November 16, 2018 · 4 min · jiezi

V8引擎是如何工作?

V8是google开发的JavaScript引擎, 它是开源的 ,而且是用C++编写的。它是用于客户端(Google Chrome)和服务器端(node.js)JavaScript应用程序。V8最初旨在提高Web浏览器中JavaScript执行的性能。为了提升速度,V8将JavaScript代码转换为更高效的机器语言,而不是使用解释器。它通过实现 JIT(即时编译器)将JavaScript代码编译成机器代码,就像许多现代JavaScript引擎(如SpiderMonkey或Rhino(Mozilla))所做的那样。与V8的主要区别在于它不会产生字节码或任何中间代码。本文的目的是展示和理解 V8如何工作,以便为客户端或服务器端应用程序生成优化的代码。如果您已经在问自己“我应该关心JavaScript性能吗?”那么我将回答Daniel Clifford(技术主管和V8团队经理)的一句话:“这不仅仅是让您当前的应用程序运行得更快,而是关于实现你过去从未做过的事情“。隐藏的classJavaScript是一种基于原型的语言:no classes,并且使用克隆过程创建对象(原型链)。JavaScript也是动态类型的:类型和类型信息不是显式的,属性可以动态添加到对象中或从中删除。有效访问类型和属性是V8的首要挑战。而不是使用类似字典的数据结构来存储对象属性和进行动态查找来解析属性位置(就像大多数JavaScript引擎一样),V8在运行时创建隐藏类,以便具有内部表示类型系统和改善属性访问时间。让我们有一个Point函数和两个Point对象的创建:https://p1.ssl.qhimg.com/t016…如果布局相同(这里是这种情况),则p和q属于由V8创建的相同隐藏类。这突出了使用隐藏类的另一个优点:它允许V8对属性相同的对象进行分组。这里p和q有一定的代码优化。现在,让我们假设我们想在我们的q对象之后添加一个z属性,就在它声明之后(对于动态类型语言来说这是完全没问题的)。V8将如何处理这种情况?事实上,每当构造函数声明一个属性并跟踪隐藏类的变化时,V8 就会创建一个新的隐藏类。为什么?因为如果创建了两个对象(p和q)并且在创建后将成员添加到第二个对象(q),则V8需要保留最后创建的隐藏类(对于第一个对象p)并创建一个新对象(对于第二个对象q)与新成员。https://p4.ssl.qhimg.com/t01c…每次创建一个新的隐藏类时,前一个隐藏类都会更新一个类转换,指示必须使用哪个隐藏类。因此:初始化构造函数中的所有对象成员(因此实例稍后不会更改类型)始终以相同的顺序初始化对象成员代码优化因为V8为每个属性创建一个新的隐藏类,所以应该将隐藏的类创建保持在最低限度。为此,请尽量避免在创建对象后添加属性,并始终以相同的顺序初始化对象成员(以避免创建不同的隐藏类树)。[Update ]另一个技巧:单态操作是仅对具有相同隐藏类的对象起作用的操作。当我们调用一个函数时,V8会创建一个隐藏类。如果我们用不同的参数类型再次调用它,V8需要创建另一个隐藏类:首选单态代码到多态代码有关V8如何优化JavaScript代码的更多示例标记值为了有效地表示数字和JavaScript对象,V8表示具有 32位值。它使用一个位来知道它是一个对象(flag = 1)还是一个整数(flag = 0),这里称为SMall Integer或 SMI ,因为它的31位。然后,如果数值大于31位,则V8将对该数字进行选择,将其变为双精度并创建一个新对象以将数字放入其中。代码优化:尽可能使用31位带符号数字,以避免对JavaScript对象进行消耗性能的封装操作。数组V8使用两种不同的方法来处理数组:快速元素:专为那些键组非常紧凑的阵列而设计。它们具有线性存储缓冲区,可以非常有效地访问它。字典元素:专为稀疏数组而设计,它们内部没有所有元素。它实际上是一个哈希表,它的性能消耗比“快速元素”更昂贵。代码优化:确保V8使用“快速元素”来处理数组,换句话说,避免使用稀疏数组。另外,尽量避免预先分配大型数组。最后,不要删除数组中的元素:它使键集稀疏。 a = new Array();for (var b = 0; b < 10; b++) { a[0] |= b; // Oh no!}//vs.a = new Array();a[0] = 0;for (var b = 0; b < 10; b++) { a[0] |= b; // Much better! 2x faster.}此外,双精度阵列更快 - 数组的隐藏类跟踪元素类型,并且仅包含双精度的数组是未装箱的(这会导致隐藏的类更改)。但是,由于装箱和拆箱,粗心操作阵列会导致额外的工作 - 例如var a = new Array();a[0] = 77; // Allocatesa[1] = 88;a[2] = 0.5; // Allocates, convertsa[3] = true; // Allocates, converts效率低于:var a = [77, 88, 0.5, true];V8如何编译JavaScript代码?V8有两个编译器!一个“完整”编译器,可以为任何JavaScript生成良好的代码。此编译器的目标是快速生成代码。为了实现其目标,它不进行任何类型分析,也不了解类型。相反,它使用内联缓存或“IC”策略来在程序运行时优化有关类型的知识。IC效率非常高,速度可提高20倍。优化编译器,可为大多数JavaScript语言生成出色的代码。它稍后会重新编译热门功能。优化编译器从内联缓存中获取类型,并决定如何更好地优化代码。但是,某些语言功能尚不支持,例如try / catch块。(try / catch块的解决方法是在函数中编写“非稳定”代码并在try块中调用函数)代码优化:V8还支持去优化:优化编译器从内联缓存中对不同类型做出假设,如果这些假设无效则会进行去优化。例如,如果生成的隐藏类不是预期的类,则V8会抛弃优化的代码并返回到完整编译器以从内联缓存中再次获取类型。此过程很慢,应该通过在优化后尝试不更改功能来避免。资源谷歌I / O 2012“与V8打破JavaScript速度限制”,V8团队的技术主管兼经理Daniel Clifford:视频和幻灯片。V8:一个开源JavaScript引擎:Lars Bak,V8核心工程师的视频。Nikkei Electronics Asia博客文章:为什么新的谷歌V8引擎如此之快?博客评论由Disqus提供译者注:关于本文中提到的一些知识点,做一些简单的只是扩展,希望对你们理解本文有一些帮助;1、 “JavaScript has no classes"虽然JavaScript是面向对象的语言,但它不是基于类的语言 - 它是基于原型的语言。在js和java或其他“基于类”的编程语言中类的工作方式之间存在一些深刻的差异。相关讨论2、快速元素和字典元素快速或字典元素:元素的第二个主要区别是它们是快速还是字典模式。快速元素是简单的VM内部数组,其中属性索引映射到元素存储中的索引。但是,这种简单的表示对于非常大的稀疏/多孔数组而言是相当浪费的,其中只占用很少的条目。在这种情况下,我们使用基于字典的表示来节省内存,但代价是访问速度稍慢:const sparseArray = [];sparseArray[9999] = ‘foo’; // Creates an array with dictionary elements.sparseArray.length// 10000sparseArray[0]// undefined在这个例子中,分配一个包含10k条目的完整数组会相当浪费。相反,V8会创建一个字典来存储键值描述符三元组。在这种情况下,密钥是'9999’,并且使用值’foo’和默认描述符。鉴于我们没有办法在HiddenClass上存储描述符详细信息,只要您使用自定义描述符定义索引属性,V8就会转向减慢元素:const array = [];Object.defineProperty(array, 0, {value: ‘fixed’ configurable: false});console.log(array[0]); // Prints ‘fixed’.array[0] = ‘other value’; // Cannot override index 0.console.log(array[0]); // Still prints ‘fixed’.引用文档 ...

November 11, 2018 · 1 min · jiezi