当前位置: 首页 > Web前端 > JavaScript

GC垃圾回收和V8引擎如何执行Javascipt代码过程

时间:2023-03-27 01:35:48 JavaScript

基本概念Javascript中的垃圾js中的内存管理是自动的内存不再被引用时就是垃圾无法从根访问对象时就是垃圾Javascript中的Reachable对象可访问对象是可达对象(引用、作用域链)。可达性的标准是能否从根找到它们。Javascipt中的root可以理解为整个团队变量对象。什么是GC算法?什么是GC垃圾:程序中不再需要的对象和程序中不能再访问的对象GC是一种机制。垃圾收集器所做的具体工作是寻找垃圾释放空间,而空间回收算法就是工作期间的搜索和回收。常用的GC算法引用计数方式的核心思想是在内部通过一个引用计数器来维护当前对象的引用数,通过判断当前引用数是否为0来判断是否是垃圾对象。当引用号为0时,GC开始工作,对其所在的内存空间进行垃圾回收。当某种引用关系发生变化时,引用计数器主动改变引用计数值。为0时,GC开始工作,回收空间。优缺点:发现垃圾后立即回收,减少程序卡顿时间。无法回收循环引用的对象(a指向b,b指向a)。时间开销大(需要维护数值变化,对象多的时候,维护会比较慢,相对于其他算法,时间开销大)mark-and-clear方法的核心思想分为两种阶段:标记和清除。第一阶段遍历所有对象,标记活动对象(可达对象),第二阶段遍历所有对象,清除未标记对象,擦除已标记对象上的标记,回收相应空间。最后将回收的空间放到空闲列表中,方便程序继续申请空间。优缺点与引用计数方式相比,循环引用对象的回收空间可以是碎片化的,回收的空间地址可能是不连续的。垃圾对象不会立即被回收。标记排序算法可以理解为标记清除算法的增强。标记阶段与标记清除方法相同。清除阶段会先进行排序,然后移动对象位置,使空闲地址连续。优缺点:减少碎片空间不会立即回收垃圾对象移动对象位置,回收效率慢V8垃圾回收策略V8内存有上限,64位操作系统不超过1.5G,32位系统不超过800M。利用分代回收的概念,将内存分为新生代和老年代,针对不同的对象使用不同的算法(新生代使用特定的GC算法,老年代使用特定的算法)。常用的GC算法包括分代回收、空间复制、标记清除、标记排序和标记递增。新生代存储区对象回收:存放新生代对象的小空间(64位系统32M,32位系统16M),新生代指的是短命对象。恢复后,使用复制算法和标记。小空间分为From空间(已用空间)和To(空闲空间)。这两个空间大小相等。当空间被使用时,它被存储在From空间中。当内存使用达到一定程度时,触发GC操作,用标记排序算法标记From空间。排序后,将活动对象复制到To空间,然后交换From和To之间的空间使用。Promotion可能发生在recoverydetails的copy过程中(一个在newgeneration中使用的对象在oldgeneration中也被使用)。如果新生代中的对象经过一轮GC操作后仍然存活,则将新生代中的对象移动到老年代。到空间使用率超过25%,当它被使用时,新生代区域就不够用了。这时候,它就会移动到老年代区。位系统700M。老年代对象是指存活时间较长的对象(全局对象、变量数据放在闭包中),主要使用标记清除、标记排序、标记递增算法。首先,使用标记清除来完成垃圾空间的回收。这时候就出现了垃圾碎片化的问题。当新生代对象提升时,老年代区域空间不够用。这时触发标记排序优化空间,整理出碎片空间(老年代区主要用算法),最后使用增量标记优化效率(程序运行时会有停顿,有会是多次停顿的时间间隔,在多次停顿的间隔中,会遍历对象进行标记和清除)。新老代垃圾回收对比新生代区域垃圾回收用空间换时间(小空间)老年代区域垃圾回收不适合复制算法V8演进历史2008年,V8第一版发布。当时V8架构比较激进。它直接将JavaScript代码编译成机器码执行,所以执行速度非常快。但是,在这个架构中,V8只有Codegen作为编译器,代码优化非常有限。2010年,V8发布了Crankshaft编译器。JavaScript函数通常首先由Full-Codegen编译。如果该函数随后被多次执行,则使用Crankshaft重新编译生成更优化的代码,然后使用优化后的代码。执行以进一步提高性能。但是Crankshaft对代码的优化有限,所以在2015年V8加入了TurboFan。V8仍然是直接将源代码编译成机器码的架构。这种架构的核心问题是内存消耗非常大。尤其是在移动设备上,Full-Codegen编译的机器码几乎占了整个Chrome浏览器的三分之一。2016年,V8加入了Ignition解释器,重新引入了字节码,旨在减少内存占用。2017年,V8正式发布了新的编译流水线,使用Ignition和TurboFan的组合来编译和执行代码。从V85.9版本开始,早期的Full-Codegen和Crankshaft编译器不再用于执行JavaScript。其中,最核心的是三个模块:解析器(Parser)、解释器(Ignition)、优化编译器(TurboFan)AbstractSyntaxTree),解释器(Ignition)将AST翻译成字节码,边解释边执行。在此过程中,解释器计算一段特定代码的运行次数。如果代码运行次数超过一定阈值,则将这段代码标记为热代码(hotcode),并将运行信息反馈给优化编译器(TurboFan)。优化编译器根据反馈的信息对字节码进行优化编译,最终生成优化后的机器码,这样再次执行代码时,解释器直接使用优化后的机器码执行,无需重新解释,大大提高了性能代码运行效率。这种在运行时编译代码的技术也被称为JIT(即时编译),JIT可以大大提高JavaScript代码的执行性能。解析器(Parser)如何将源码转换成AST要让V8执行我们写的源码,我们需要将源码转换成V8可以理解的格式。V8首先会将源代码解析成抽象语法树(abstractsyntaxtree,AST),这是一个用来表示源代码树状结构的对象。这个过程称为解析,主要由V8的Parser模块实现。然后,V8的解释器会把AST编译成字节码,边解释边执行。解析和编译过程的性能非常重要,因为V8只有编译后才能运行代码(目前我们主要关注V8中解析过程的实现)。整个分析过程可以分为两部分。词法分析:将字符流转化为记号。字符流就是我们写的那一行代码。Token是指语法上不可分割的最小单位。它可以是单个字符或字符串。图中的Scanner就是V8词法分析器。语法分析:根据语法规则,将标记组成一个层次嵌套的抽象语法结构树。这棵树是AST。在此过程中,如果源代码不符合语法规范,解析过程将终止并抛出语法错误。图中的Parser和Pre-Parser都是V8语法分析器。词法分析在V8中,Scanner负责接收Unicode字符流并将其解析为token以供解析器使用。例如,对于代码行vara=1;,经过词法分析后的标记如下:[{"type":"Keyword","value":"var"},{"type":"Identifier","value":"a"},{"type":"Punctuator","value":"="},{"type":"Numeric","value":"1"},{"type":“标点符号”,“价值”:“;”}]可以看到,vara=1;这样一行代码包括5个token:语法分析接下来V8解析器会根据token通过语法分析生成AST,vara=1;这行代码生成的AST的JSON结构如下:AST在线转换网站{"type":"Program","start":0,"end":10,"body":[{"type":"VariableDeclaration","start":0,"end":10,"declarations":[{"type":"VariableDeclarator","start":4,"end":9,"id":{"type":"标识符","start":4,"end":5,"name":"a"},"init":{"type":"Literal","start":8,"end":9、“值”:1、"raw":"1"}}],"kind":"var"}],"sourceType":"module"}但是,对于一个JavaScript源代码,如果在执行之前必须将所有源代码完全解析,它必然会面临以下问题:代码执行时间会变长:一次性解析所有代码,必然会增加代码的运行时间。消耗更多内存:解析出的AST和根据AST编译出的字节码都会存放在内存中,必然会占用更多的内存空间。占用磁盘空间:编译后的代码会缓存在磁盘上,占用磁盘空间。因此,现在主流的JavaScript引擎都实现了LazyParsing。延迟解析延迟解析的思路很简单:在解析过程中,对于不是立即执行的函数,只进行预解析(PreParser),只有在函数被调用时,才对函数进行完整的解析.进行预解析时,只校验函数语法是否有效,解析函数声明,判断函数作用域,不生成AST,实现预解析,即Pre-Parser解析器。以一段代码为例:functionfoo(a,b){varres=a+b;返回res;}vara=1;变量c=2;富(1,2);由于Scanner读取代码是从上到下逐行读取的,所以V8解析器也是从上到下解析代码。当V8解析器遇到函数声明foo时,发现并没有立即执行,所以会用Pre-Parser解析器对其进行预解析。在此过程中,只会解析函数声明,不会解析函数内部代码。代码生成AST。然后Ignition解释器会将AST编译成字节码并执行。解释器会按照自上而下的顺序执行代码,先执行vara=1;和vara=2;两个赋值表达式,然后执行函数Callfoo(1,2),然后Parser解析器会继续解析函数中的代码,生成AST,然后交给Ignition解释器编译执行。解释器(Ignition)如何将AST翻译成字节码并执行在V8架构的演进中,我提到V8引入字节码来解决内存使用问题。如图所示,通常一个几KB的文件转换成机器码可能是几十兆,占用的内存空间巨大。V8的字节码是机器码的抽象。语法有点类似于汇编。您可以将V8字节码视为指令。这些指令组合起来就实现了我们编写的功能。V8定义了数百个字节。代码,可以在V8解释器的头文件中查看所有字节码,bytecode.hIgnition解释器在执行字节码时主要使用通用寄存器和累加器寄存器,函数参数和局部变量存放在通用寄存器中,累加器寄存器用于保存中间结果。如果安装了Node.js,可以通过node--print-bytecodeindex.js命令查看JavaScript文件生成的字节码,会输出很多信息,重点关注文件末尾的字节码。node--print-bytecodeindex.js优化编译器(TurboFan)的工作原理为了提高JavaScript的执行性能,V8在优化编译方面做了很多工作,其中最重要的两个算法是内联和逃逸分析。functionadd(x,y){returnx+y;}functionthree(){returnadd(1,2);}如果不做优化直接编译代码,会分别生成两个函数的机器码。但是为了进一步提高性能,TurboFan优化编译器首先将上述两个函数内联,然后再进行编译。由于函数3的内部行为是求1和2的和,所以上面的代码等价于:functionthree_add_inlined(){varx=1;变量y=2;varadd_return_value=x+y;返回add_return_value;}更进一步,由于函数three_add_inlined中x和y的值是确定的,所以three_add_inlined可以进一步优化,直接返回结果3:functionthree_add_const_folded(){return3;}这样,three_add_inlined生成的机器码最终编译和优化前相比,少了很多,执行效率自然高很多。通过内联,您可以降低复杂性、消除冗余代码、合并常量,而内联技术通常是逃逸分析的基础。逃逸分析(EscapeAnalysis)分析对象的生命周期是否局限于当前函数。classPoint{constructor(x,y){this.x=x;这个.y=y;}distance(that){returnMath.abs(this.x-that.x)+Math.abs(this.y-that.y);}}functionmanhattan(x1,y1,x2,y2){consta=newPoint(x1,y1);常量b=新点(x2,y2);returna.distance(b);}我们定义了一个Point类,用来表示某个点的坐标。类中有一个距离方法,用于计算两点之间的曼哈顿距离。然后我们在曼哈顿函数中新建两个点a和b,并计算ab的曼哈顿距离。TurboFan首先通过内联将manhattan函数转换为以下函数:functionmanhattan_inlined(x1,y1,x2,y2){consta={x:x1,y:y1};constb={x:x2,y:y2};returnMath.abs(a.x-b.x)+Math.abs(a.y-b.y);}接下来对manhattan_inlined中的对象进行逃逸分析。两种类型的对象将被视为函数内部定义的“未转义”对象;对象只在函数内部作用域,如:不返回,不传递给其他函数等。在manhattan_inlined中,变量a和b在函数内部是普通对象,所以是“非转义”对象。然后我们可以替换函数中的对象,使用标量替换对象:functionmanhattan_scalar_eplacement(x1,y1,x2,y2){vara_x=x1;vara_y=y1;varb_x=x2;varb_y=y2;returnMath.abs(a_x-b_x)+Math.abs(a_y-b_y);}这样,函数中就没有对象定义了,a_xa_yb_xb_y被a_xa_yb_xb_y代替,直接导出从函数参数。这样做的好处是我们可以直接将变量加载到寄存器中,不再需要从内存中访问对象属性,提高了执行效率,减少了内存占用。总结V8执行JavaScript的原理大致分为三步:解析器将JavaScript源码解析成AST,解析过程分为词法分析和语法分析。V8通过预解析提高解析效率;interpreterIgnition根据AST生成字节码并执行。在此过程中,会收集执行反馈信息,交给TurboFan进行优化编译;TurboFan会根据Ignition收集到的反馈信息,将字节码编译成优化后的机器码,然后Ignition将字节码替换为优化后的机器码执行,从而提升性能。