一、js的工作原理:引擎、运行时与调用栈概述
JavaScript引擎的一个流行示例是Google的V8引擎。比如,V8引擎用于Chrome和Node.js。
该引擎包括两个主要组件:
*内存堆-这是内存分配的地方
*调用堆栈-这是代码执行时堆栈帧的位置
浏览器中有几乎所有JavaScript开发人员都在使用的API(例如“ setTimeout”)。但是,引擎不提供这些API。
那么,它们来自哪里?
事实证明,现实有点复杂。
因此,我们有引擎,但实际上还有更多。我们拥有由浏览器提供的称为Web API的东西,例如DOM,AJAX,setTimeout等。
然后,我们有了非常流行的事件循环和回调队列。
JavaScript是一种单线程编程语言,这意味着它具有单个调用堆栈。因此,它一次只能做一件事。
调用堆栈是一种数据结构,它基本上记录了我们在程序中的位置。如果进入函数,则将其放在堆栈的顶部。如果我们从函数返回,则会弹出堆栈顶部。这就是堆栈所能做的。
让我们来看一个例子。看下面的代码:
function multiply(x, y) { return x * y; } function printSquare(x) { var s = multiply(x, x); console.log(s); } printSquare(5);
当引擎开始执行此代码时,调用堆栈将为空。之后,将执行以下步骤:
调用堆栈中的每个条目都称为堆栈帧。
这正是抛出异常时构造堆栈跟踪的方式-基本上,这是异常发生时调用堆栈的状态。看下面的代码:
function foo() { throw new Error(‘SessionStack will help you resolve crashes :)‘); } function bar() { foo(); } function start() { bar(); } start();
如果在Chrome中执行此操作(假设此代码位于名为foo.js的文件中),则会生成以下堆栈跟踪:
“ 炸毁堆栈 ” —当您达到最大呼叫堆栈大小时,就会发生这种情况。这很容易发生,特别是如果您使用递归而不对代码进行大量测试的话。看一下以下示例代码:
function foo() { foo(); } foo();
当引擎开始执行此代码时,它首先调用函数“ foo”。但是,此函数是递归的,并且在没有任何终止条件的情况下开始调用自身。因此,在执行的每个步骤中,都将相同的函数一遍又一遍地添加到调用堆栈中。看起来像这样:
但是,在某些时候,“调用堆栈”中的函数调用数量超过了“调用堆栈”的实际大小,浏览器决定通过抛出错误来采取措施,该错误看起来像这样:
在单个线程上运行代码非常容易,因为您不必处理多线程环境中出现的复杂情况,例如死锁。
但是在单线程上运行也有很大的限制。由于JavaScript具有单个调用堆栈,所以当事情变慢时会发生什么?
当在调用堆栈中进行函数调用要花费大量时间才能处理时,会发生什么情况?例如,假设您想在浏览器中使用JavaScript进行一些复杂的图像转换。
您可能会问-为什么这甚至是问题?问题在于,尽管调用堆栈具有要执行的功能,但浏览器实际上无法执行其他任何操作-它被阻塞了。这意味着浏览器无法渲染,无法运行任何其他代码,只是卡住了。如果您想要在应用程序中使用流畅的用户界面,则会产生问题。
这不是唯一的问题。一旦您的浏览器开始处理“调用堆栈”中的许多任务,它可能会在很长一段时间内停止响应。而且大多数浏览器都会通过引发错误来采取行动,询问您是否要终止网页。
现在,那不是最好的用户体验,不是吗?
那么,如何在不阻塞UI并使浏览器无响应的情况下执行繁重的代码呢?好吧,解决方案是异步回调。
二、JavaScript的工作原理:V8引擎内部+ 5条有关编写优化代码的技巧
几周前,我们开始了一系列旨在深入研究JavaScript及其实际工作的系列:我们认为,通过了解JavaScript的构建基块以及它们如何一起发挥作用,您将能够编写更好的代码和应用程??序。该系列的第一篇文章重点介绍了引擎,运行时和调用堆栈的概述。第二篇文章将深入探讨Google V8 JavaScript引擎的内部部分。我们还将提供一些有关在构建产品时如何编写更好的JavaScript代码的快速提示。
JavaScript引擎是一个程序或执行JavaScript代码的解释器。JavaScript引擎可以实现为标准解释器,也可以作为即时编译器将JavaScript编译为某种形式的字节码。
这是实现JavaScript引擎的热门项目的列表:
由Google构建的V8引擎是开源的,并使用C++编写。该引擎在Google Chrome内部使用。但是,与其他引擎不同,V8还用于流行的Node.js运行。
V8最初旨在提高Web浏览器中JavaScript执行的性能。为了获得速度,V8将JavaScript代码转换为更有效的机器代码,而不是使用解释器。它通过像许多现代JavaScript引擎(例如SpiderMonkey或Rhino(Mozilla))一样实现JIT(即时)编译器,在执行时将JavaScript代码编译为机器代码。这里的主要区别是V8不会产生字节码或任何中间代码。
在V8 5.9版本(今年早些时候发布)发布之前,该引擎使用了两个编译器:
V8引擎还在内部使用多个线程:
首次执行JavaScript代码时,V8会利用完整的代码生成器,该代码生成器将已解析的JavaScript直接转换为机器代码,而无需进行任何转换。这使它可以非常快速地开始执行机器代码。请注意,V8不会以这种方式使用中间字节码表示,从而无需解释器。
当您的代码运行了一段时间后,探查器线程已经收集了足够的数据来告诉您应该优化哪种方法。
接下来,曲轴优化从另一个线程开始。它将JavaScript抽象语法树转换为称为Hydrogen的高级静态单一分配(SSA)表示形式,并尝试优化该Hydrogen图。大多数优化都在此级别上完成。
第一个优化是预先内联尽可能多的代码。内联是将调用站点(调用函数的代码行)替换为被调用函数的主体的过程。这个简单的步骤可使后续优化变得更有意义。
JavaScript是基于原型的语言:没有使用克隆过程创建的类和对象。JavaScript还是一种动态编程语言,这意味着可以在实例化对象后轻松地添加或删除属性。
大多数JavaScript解释器使用类似字典的结构(基于哈希函数)将对象属性值的位置存储在内存中。与在非动态编程语言(例如Java或C#)中使用JavaScript相比,使用这种结构检索属性的值在计算上更加昂贵。在Java中,所有对象属性均由编译前的固定对象布局确定,并且无法在运行时动态添加或删除(嗯,C#具有动态类型是另一个主题)。结果,属性的值(或指向这些属性的指针)可以作为连续缓冲区存储在内存中,并且在每个缓冲区之间具有固定偏移量。可以根据属性类型轻松确定偏移量的长度,但是在JavaScript中这是不可能的,因为JavaScript在运行时可以更改属性类型。
由于使用字典查找对象属性在内存中的位置效率很低,因此V8改用了另一种方法:隐藏类。隐藏类的工作方式类似于Java之类的语言中使用的固定对象布局(类),但它们是在运行时创建的。现在,让我们看看它们的实际外观:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2);
一旦发生“new Point(1,2)”调用,V8将创建一个名为“ C0”的隐藏类。
尚未为Point定义任何属性,因此“ C0”为空。
一旦执行了第一个语句“ this.x = x”(在“ Point”函数内部),V8将创建一个基于“ C0”的第二个隐藏类“ C1”。“ C1”描述了在内存中(相对于对象指针)可以找到属性x的位置。在这种情况下,“ x”存储在偏移量0处,这意味着在将内存中的点对象作为连续缓冲区查看时,第一个偏移量将对应于属性“ x”。V8还将使用“类转换”更新“ C0”,该类转换指出如果将属性“ x”添加到点对象,则隐藏的类应从“ C0”切换为“ C1”。现在,下面的点对象的隐藏类为“ C1”。
每次将新属性添加到对象时,都会使用到新隐藏类的过渡路径来更新旧的隐藏类。隐藏类转换非常重要,因为它们允许隐藏类在以相同方式创建的对象之间共享。如果两个对象共享一个隐藏类,并且向它们两个都添加了相同的属性,则过渡将确保两个对象都接收到相同的新隐藏类以及随之而来的所有优化代码。
当执行语句“ this.y = y”(同样在Point函数内部,在“ this.x = x”语句之后)时,将重复此过程。
创建了一个名为“ C2”的新隐藏类,将一个类转换添加到“ C1”,表明如果将属性“ y”添加到Point对象(已经包含属性“ x”),则该隐藏类应更改为“ C2”,并且点对象的隐藏类更新为“ C2”。
隐藏的类转换取决于将属性添加到对象的顺序。看一下下面的代码片段:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2); p1.a = 5; p1.b = 6; var p2 = new Point(3, 4); p2.b = 7; p2.a = 8;
现在,您假定对于p1和p2,将使用相同的隐藏类和转换。好吧,不是真的。对于“ p1”,将首先添加属性“ a”,然后添加属性“ b”。但是,对于“ p2”,首先分配“ b”,然后分配“ a”。因此,“ p1”和“ p2”由于不同的过渡路径而最终具有不同的隐藏类。在这种情况下,最好以相同的顺序初始化动态属性,以便可以重用隐藏的类。
V8利用了另一种用于优化动态类型语言的技术,称为内联缓存。内联缓存依赖于这样的观察,即对相同类型的对象倾向于重复调用相同的方法。可以在此处找到有关内联缓存的深入说明。
我们将介绍内联缓存的一般概念(以防您没有时间进行上面的深入说明)。
那么它是怎样工作的?V8维护最近的方法调用中作为参数传递的对象类型的缓存,并使用此信息对将来将作为参数传递的对象的类型做出假设。如果V8能够很好地假设将传递给方法的对象的类型,则它可以绕过找出如何访问对象属性的过程,而可以使用先前查找到对象的存储信息。隐藏的类。
那么隐藏类和内联缓存的概念如何相关?每当在特定对象上调用方法时,V8引擎都必须对该对象的隐藏类执行查找,以确定用于访问特定属性的偏移量。在对相同的隐藏类成功两次调用相同的方法之后,V8会省略隐藏类查找,而只是将属性的偏移量添加到对象指针本身。对于该方法的所有将来调用,V8引擎假定隐藏类未更改,并使用从以前的查询存储的偏移量直接跳到特定属性的内存地址。这大大提高了执行速度。
内联缓存也是同样重要的原因,因为相同类型的对象共享隐藏的类。如果创建两个具有相同类型且具有不同隐藏类的对象(如我们在前面的示例中所做的那样),则V8将无法使用内联缓存,因为即使这两个对象属于同一类型,它们的对应隐藏类为它们的属性分配不同的偏移量。
这两个对象基本相同,但是“ a”和“ b”属性是按不同顺序创建的。
优化Hydrogen图后,曲轴将其降低到一个称为Lithium的较低级表示形式。大多数Lithium实现都是特定于体系结构的。寄存器分配发生在此级别。
最后,锂被编译为机器代码。然后发生了另一件事,称为OSR:堆栈替换。在我们开始编译和优化之前,我们可能已经在运行它。V8不会忘记它只是缓慢执行以重新启动优化版本。相反,它将转换我们拥有的所有上下文(堆栈,寄存器),以便我们可以在执行过程中切换到优化版本。考虑到在其他优化中,V8最初已内联代码,这是一项非常复杂的任务。V8不是唯一能够做到这一点的引擎。
有一种称为反优化的保护措施,可以进行相反的转换,并在假设引擎不再适用的情况下还原为未优化的代码。
对于垃圾收集,V8使用标记清除的传统世代方法来清理旧世代。标记阶段应该停止JavaScript执行。为了控制GC成本并使执行更加稳定,V8使用了增量标记:不是遍历整个堆而是尝试标记每个可能的对象,是遍历堆的一部分,然后恢复正常执行。下一个GC停止将从上一个堆漫游已停止的位置继续。这允许在正常执行期间非常短的暂停。如前所述,清除阶段由单独的线程处理。
随着2017年初V8 5.9的发布,引入了新的执行管道。这个新的管道在实际的 JavaScript应用程序中实现了更大的性能改进并显着节省了内存。
新的执行管道基于V8的解释器Ignition和V8的最新优化编译器TurboFan构建。
您可以在此处查看 V8团队有关该主题的博客文章。
自V8的5.9版问世以来,由于V8团队一直在努力跟上新的JavaScript语言功能,并且V8团队一直在努力与之同步,因此完整代码源和Crankshaft(自2010年起为V8服务的技术)不再被V8用于JavaScript执行。这些功能所需的优化。
这意味着整个V8将会拥有更简单,更易于维护的体系结构。
这些改进仅仅是开始。新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提高JavaScript性能并缩小V8在Chrome和Node.js中的占用空间。
最后,这是有关如何编写经过优化的更好JavaScript的一些技巧。您可以从上面的内容中轻松获得这些内容,但是,为方便起见,以下是摘要:
三、JavaScript的工作方式:内存管理+如何处理4个常见的内存泄漏
几周前,我们开始了一系列旨在更深入地研究JavaScript及其实际工作的系列:我们认为,通过了解JavaScript的构造块以及它们如何一起发挥作用,您将能够编写更好的代码和应用程??序。
该系列的第一篇文章重点介绍了引擎,运行时和调用栈的概述。Thе 第二后仔细研究谷歌的V8 JavaScript引擎的内部零件,也提供了有关如何写出更好的JavaScript代码的一些提示。
在这第三篇文章中,我们将讨论另一个关键主题,由于每天使用的编程语言的日趋成熟和复杂性日益增加,因此开发人员越来越不重视它了,即内存管理。我们还将提供一些有关如何处理SessionStack中JavaScript内存泄漏的技巧,因为我们需要确保SessionStack不会导致内存泄漏或不会增加集成的Web应用程序的内存消耗。
像C这样的语言具有诸如malloc()
和的低级内存管理原语free()
。这些原语由开发人员用来在操作系统之间显式分配和释放内存。
同时,JavaScript在创建事物(对象,字符串等)时分配内存,并在不再使用它们时“自动”释放内存,此过程称为垃圾回收。释放资源的这种看似“自动”的特性引起了混乱,并给JavaScript(和其他高级语言)开发人员带来了错误的印象,他们可以选择不关心内存管理。 这是一个大错误。
即使使用高级语言,开发人员也应该对内存管理有所了解(或至少是基础知识)。有时,自动内存管理存在一些问题(例如错误或垃圾收集器中的实现限制等),开发人员必须了解这些问题,以便正确处理它们(或找到一种适当的解决方法,并以最小的权衡和代码来实现)。
—— 未完 ——
本文译自:
1.https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
原文:https://www.cnblogs.com/bbcfive/p/12208044.html