Firefox已是内存使用最高效的浏览器之一,然而Mozilla工程师仍在继续节省内存:Javascript引擎实现了单线程化。

简介

自从关心了nodejs以后,然后就开始关心javascript,然后就开始关心浏览器的内部原理,然后就没有然后了
— —!

以下就是心路历程:

路人甲: JavaScript是单线程的!
Me: 哇哦!,原来他是单线程的,哦,然后呢?

路人已:JavaScript是事件轮询的!
Me: 哦,知道了,然后呢?

路人丙:为什么JavaScript是单线程的呢?
Me:
额……不知道,→_→,JavaScript不就是写用户交互操作的脚本吗?我管他是不是单线程的。


时间: 2019-08-29阅读: 209标签: 框架

旧的JavaScript运行时支持多线程,需要复杂的线程/锁定代码确保数据能正确访问,结果是导致难以维护,消耗更多内存,降低垃圾收集速度。

为什么JavaScript是单线程的?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
澳门新葡萄京所有网站,所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web
Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

对于任何一个程序员来说,最关注的两个问题无非就是:时间复杂度和空间复杂度。第一部分介绍了
V8 为改进 JavaScript
执行时间所做的速度提升和优化,第二部分则将着重介绍内存管理方面的知识。这篇文章,小编简要概述了编程语言的一般工作机制,并深入探讨了
V8 引擎的管道。第二部分将介绍一些更重要的概念,这些概念是每一个
JavaScript 程序员都必须了解的,并且不仅仅和 V8 引擎有关。

新的JavaScript运行时采用单线程,每个WebWorkers都有自己的单线程JS运行时实例,每个运行时因此能简化表达,引擎能在线程之间优化工作,能更好的执行代码生成,更快的垃圾收集。

浏览器的主要构成

浏览器的主要构成,先上图 我们再扯淡:

澳门新葡萄京所有网站 1

浏览器简构

我个人大致把浏览器的构成分为三大部分:
  1. 用户界面
  
  2.
浏览器内核(浏览器引擎,渲染引擎,JavaScript引擎(也叫JavaScript解析器),网络)
   其中渲染引擎用来解析执行Html与CSS的
   JavaScript引擎是用来解析执行JavaScript代码
  
  3. 保存类似Cookie的各种数据


浏览器的主要组件包括:
  1. 用户界面 -
包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分。
  2. 浏览器引擎 - 用来查询及操作渲染引擎的接口。
  3. 渲染引擎 -
用来显示请求的内容,例如,如果请求内容为html,它负责解析html及css,并将解析后的结果显示出来。
  4. 网络 -
用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作。
  5. UI后端 -
用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口。
  6. JavaScript解释器 - 用来解释执行JS代码。
  7. 数据存储 -
属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web
database技术,这是一种轻量级完整的客户端存储技术


内存堆

(文/solidot)    

闲谈-为什么说Chrome运行很快

Chrome的浏览器内核用的是Webkit,JavaScript引擎用的是V8

在Chrome中,只有Html的渲染采用了WebKit的代码,而在JavaScript上,重新搭建了一个NB哄哄的V8引擎。目标是,用WebKit

  • V8的强强联手,打造一款上网冲浪的法拉利.
    (1)
    V8在执行之前将JavaScript编译成了机器码,而非位元组码或是直译它,以此提升效能。更进一步,使用了如内联缓存(inline
    caching)等方法来提高性能。有了这些功能,JavaScript程序与V8引擎的速度媲美二进制编译。

(2)
传统的javascript是动态语言.JavaScript继承方法是使用prototype,透过指定prototype属性,便可以指定要继承的目标。属性可以在运行时添加到或从对象中删除,引擎会为执行中的物件建立一个属性字典,新的属性都要透过字典查找属性在内存中的位置。V8为object新增属性的时候,就以上次的hidden
class为父类别,创建新属性的hidden
class的子类别,如此一来属性访问不再需要动态字典查找了。

(3) 为了缩短由垃圾收集造成的停顿,V8使用stop-the-world,
generational, accurate的垃圾收集器

Orinoco 的 logo:V8 的垃圾回收器

JavaScript引擎(JavaScript解释器)

还是老规矩,先上图 ,再扯淡:

澳门新葡萄京所有网站 2

javascript引擎

(1).JavaScript 引擎的基本工作是把开发人员写的 JavaScript
代码转换成高效、优化的代码。它主要就是,分析、解释、优化、垃圾回收
JavaScript 代码

(2).JavaScript引擎是一个“进程虚拟机”,它给JavaScript代码提供了运行环境,用于执行JavaScript代码

(3).JavaScript引擎是单线程的,维护了一个事件队列(和浏览器事件轮询有关系)

(4).JavaScript引擎根据ECMAScript定义的语言的标准来实现

注:
“虚拟机”是指软件驱动的给定的计算机系统的模拟器。有很多类型的虚拟机,它们根据自己在多大程度上精确地模拟或代替真实的物理机器来分类。
“系统虚拟机”提供了一个可以运行操作系统的完整仿真平台
“进程虚拟机”不具备全部的功能,能运行一个程序或者进程

每当你在 JavaScript
程序中定义了一个变量、常量或者对象时,你都需要一个地方来存储它。这个地方就是内存堆。

事件轮询-Event Loop

Event Loop:其实也就是JavaScript引擎一直在执行任务队列中的任务。

由于JavaScript引擎是单线程的,单线程意味着所有任务需要排队,前一个任务结束,才会执行下一个任务,假设第一个任务是执行(a++;),第二个任务是通过ajax从网络读取数据-[很耗时的任务],假如直接放在JavaScript引擎中执行,那么JavaScript引擎会一直等待服务端的数据(这时就阻塞线程了),JavaScript引擎会一直等待到数据从服务端传递过来才会执行下一个任务,同时,在JavaScript等待的时候,CPU是空闲的,大大的资源浪费啊!,也会出现界面“假死”状态,那么怎么办呢?

解:浏览器内核是多线程的
1.Ajax操作是由浏览器内核中的浏览器Http异步线程执行的:发送—-等待—-接收
2.JavaScript引擎遇到Ajax操作会交给浏览器内核中的Http异步线程执行,从而自身继续执行下面的任务
3.当Http异步线程接收到数据以后,数据会以回调函数的形式放入任务队列中,JavaScript下次空闲的时候执行该回调函数。

异步任务简单点说
就是不占用当前线程,通过当前线程交给其他线程处理的任务,其他线程处理完毕后,再以回调函数的方式通知当前线程。

澳门新葡萄京所有网站 2

javascript引擎

JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序,但是浏览器内核是多线程的

(1).
浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步.假如某一浏览器内核的实现至少有三个常驻线程:javascript引擎线程,界面渲染线程,浏览器事件触发线程,除些以外,也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件.
(2).
界面渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.本文虽然重点解释JavaScript定时机制,但这时有必要说说渲染线程,因为该线程与JavaScript引擎线程是互斥的,这容易理解,因为
JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了.
在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被“冻结”了.

接下来我们用代码来论证上面所说的:

澳门新葡萄京所有网站 4

执行js时刻图

                window.onload = function() {

            var date1 = new Date();
            //异步任务-js引擎线程发现setTimeout这个方法,然后通知浏览器内核启动浏览器定时线程,浏览器定时线程开始定时,js引擎线程执行这个代码块只花了不到1毫秒的时间,然后js引擎就继续往下执行
            //当定时线程到了30s后,就把回调函数放在js引擎队列里面,JS引擎会一直遍历自己的队列,是否有任务要处理,如果js引擎队列正在执行其他方法,那么该回调函数就会等其他任务执行完了再执行,如果js引擎是空闲的,那么就会立即执行
            setTimeout(function() {
                alert("setTimeOut Finish");
            }, 1000 * 30);

            var date2 = new Date();
            var haomiao = date2.getTime() - date1.getTime();
            console.log(date1.getMilliseconds() - date2.getMilliseconds())
//同步任务-立即执行 不会等待30秒后再执行
var a=0;
console.log(a++);
            //同步任务-立即执行,当JS引擎执行在这里的时候,JS引擎是空闲的,JS引擎就立即执行该方法 ,因为每次循环都在占用js线程,所以js引擎不会执行下面的方法
            delayTwentyMilliseconds();

            //异步方法-js引擎 -发现ClickMe() 交给浏览器内核,浏览器内核再交给浏览器事件触发线程,浏览器事件触发线程就会注册点击事件ClickMe,然后Js引擎就不管,然后js引擎就继续往下执行
            function clickMe() {
                var date1 = new Date();
                var date2 = new Date();
                var haomiao = date2.getTime() - date1.getTime();
                alert("点击完成时间执行完成 耗时:" + haomiao / 1000 + '秒');

            }
            //异步方法
            setTimeout(function() {
                alert("setTimeOut Finish");
            }, 1000 * 30);
        }


//定义执行一秒的同步方法
        function delayOneMilliseconds() {
            for (var i = 1; i < 1000; i++) {
                for (var j = 1; j < 10; j++) {
                    for (var k = 1; k < 10; k++) {
                        var b = k * 10;
                    }
                }
            }
        }
//定义执行20秒的同步方法
        function delayTwentyMilliseconds() {
            for (var i = 1; i < 10000; i++) {
                for (var j = 1; j < 1000; j++) {
                    for (var k = 1; k < 2000; k++) {
                        var b = k * 100;
                    }
                }
            }
        }

总结:
1.JavaScript引擎一直在执行任务队列中的任务,当遇到同步任务的时候会立即执行,遇到异步任务会交给浏览器内核的其他线程执行,当其他线程执行完毕,会以回调函数任务的形式放入到JavaScript任务队列中,JavaScript引擎会继续往下执行,当JavaScript引擎空闲时会执行回调函数任务。
2.delayTwentyMilliseconds().这个算是大规模的运算操作,执行时间是20s,[同步任务],会一直阻塞JavaScript线程,从而说明了为什么NodeJS不适合做大规模的运算操作

当遇到语句 var a = 10 的时候,内存会分配一个位置用于存储 a 的值

浏览器与NodeJS

NodeJs和浏览器是差不多的,Node.js也是单线程的Event
Loop.只不过浏览器是浏览器内核来管理异步线程,NodeJs是libuv这模块来管理异步线程,同样的NodeJS也是利用V8-JavaScript引擎来进行执行任务队列-Event
Loop

澳门新葡萄京所有网站 5

NodeJS大体模块

NodeJS能处理高并发连接并且达到比较良好的吞吐量的真正原因:
<font color=red>以下是年轻的时候的错误理解</font>:
实际上是NodeJS中的http模块是异步的,NodeJS只负责接收海量的Http请求连接,而处理这些连接是由libuv来处理,只是把压力转给了libuv。
NodeJS对数据库的操作也是同样的道理。

<font color=green>正解:</font>
其实看上面的论述,可以得知不管是NodeJS还是浏览器的JavaScript引擎都是单线程的,所以所有的请求都会排队
然而,真正提高并发吞吐量的是I/O多路复用(解释:I/O多路复用其意思就是所有的接受请求响应都是用同一个线程来处理),正因为Node接受用户请求时是用一个线程接收所有请求的(不需要开其他线程来处理请求),由于每个进程/线程需要占用不少资源(典型的是内存,一个线程通常需要2M的栈空间),更重要的是,线程/进程创建,切换,销毁时的开销是非常大的。
然而Node的异步事件轮训:
1.异步:
异步表现在于Node在处理比较耗时的I/O(比如请求第三方API,读写文件,读写数据)的时候,Node使用异步回调的形式来处理,这样当遇见比较耗时的I/O时,Node不会等待,而是继续接受其他用户的请求,从而达到更高的并发吞吐量。
2.事件轮训:
事件轮训表现在于,libuv也维护了一个事件队列,所有比较耗时的I/O操作都由libuv来处理,同时libuv一直轮训事件队列的事件是否完成(因为所有事件都是异步的只能轮训),然后以回调函数的方式及时响应给JavaScript解释器

在此只能说libuv很强 敬礼!!!

可用内存是有限的,而复杂的程序可能有很多变量和嵌套对象,因此合理地使用可用内存非常重要。

window.onload

界面渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.本文虽然重点解释JavaScript定时机制,但这时有必要说说渲染线程,因为该线程与JavaScript引擎线程是互斥的,这容易理解,因为
JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了.

流程:
渲染引擎解析Html和CSS同时,如果这个时候JavaScript引擎在操作Html元素,浏览器是相信渲染引擎的还是JavaScript引擎的?所以window.onload就解决了,当渲染引擎执行完页面渲染,才执行JavaScript引擎来执行JavaScript脚本

和诸如 C 这种需要显式分配和释放内存的语言不同,JavaScript
提供了自动垃圾回收机制。一旦对象/变量离开了上下文并且不再使用,它的内存就会被回收并返还到可用内存池中。

回调函数

回调函数 :其实就是函数指针 无异步,无回调,解决了异步函数传值的问题

  //假如A是异步方法。
    function A(callback){
        var a=0;
            //假设执行异步任务是a++,
            a++;
            //当异步任务执行完,调用callback这个函数(也就是B(biu)这个函数),所以只能通过回调函数的方法把a值传递给B,方便B来操作数据
            callback(a);
        }
        //function B(piu)是一个函数,不是函数指针,bb才是函数指针
        var bb=function B(piu){
            piu++;
            alter(piu);
        }

        A(bb);
----------

A(bb){
var a=0;
//假设执行异步任务是a++,
a++;
B(a){
a++;
alter(a);
}
}
//方法B被方法A回调,方法B是方法A的回调函数
//流程:由于A是异步函数,由浏览器内核来执行这个函数,当浏览器内核执行完毕,会把回调函数放入任务队列中,javaScript引擎在空闲的时候就会执行这个回调函数

        //假如A是异步方法。
    function A(callback){
            var a=0;
            //假设执行异步任务是a++,
            a++;
            //如果这样 返回的a将等于0.
            retrun a;
        }

在 V8 中,垃圾回收器的名字叫做
Orinoco,它的处理过程非常高效。这篇文章有相关解释

标记与清除算法

标记和清除算法

我们通常会使用这种简单有效的算法来判定可以从内存堆中安全清除的对象。算法的工作方式正如其名:将对象标记为可获得/不可获得,并将不可获得的对象清除。

垃圾回收器周期性地从根部或者全局对象开始,移向被它们引用的对象,接着再移向被这些对象引用的对象,以此类推。所有不可获得的对象会在之后被清除。

内存泄漏

虽然垃圾回收器很高效,但是开发者不应该就此将内存管理的问题束之高阁。管理内存是一个很复杂的过程,哪一块内存不再需要并不是单凭一个算法就能决定的。

内存泄漏指的是,程序之前需要用到部分内存,而这部分内存在用完之后并没有返回到内存池。

下面是一些会导致你的程序出现内存泄漏的常见错误:

全局变量:如果你不断地创建全局变量,不管有没有用到它们,它们都将滞留在程序的整个执行过程中。如果这些变量是深层嵌套对象,将会浪费大量内存。

vara={...}varb={...}functionhello(){c=a;//这是一个你没有意识到的全局变量}

如果你试图访问一个此前没有声明过的变量,那么将在全局作用域中创建一个变量。在上面的例子中,c
是没有使用 var 关键字显式创建的变量/对象。

事件监听器:为了增强网站的交互性或者是制作一些浮华的动画,你可能会创建大量的事件监听器。而用户在你的单页面应用中移向其他页面时,你又忘记移除这些监听器,那么也可能会导致内存泄漏。当用户在这些页面来回移动的时候,这些监听器会不断增加。

varelement=document.getElementById('button');element.addEventListener('click',onClick)

Intervals 和
Timeouts:当在这些闭包中引用对象时,除非闭包本身被清除,否则不会清除相关对象。

setInterval(()={//引用对象}//这时候忘记清除计时器//那么将导致内存泄漏!

移除 DOM 元素:这个问题很常见,类似于全局变量导致的内存泄漏。DOM
元素存在于对象图内存和 DOM 树中。用例子来解释可能会更好:

varterminator=document.getElementById('terminate');varbadElem=document.getElementById('toDelete');terminator.addEventListener('click',function(){memorybadElem.remove();});

在你通过 id = ‘terminate’ 点击了按钮之后,toDelete 会从 DOM
中移除。不过,由于它仍然被监听器引用,为这个对象分配的内存并不会被释放。

varterminator=document.getElementById('terminate');terminator.addEventListener('click',function(){varbadElem=document.getElementById('toDelete');badElem.remove();});

badElem 是局部变量,在移除操作完成之后,内存将会被垃圾回收器回收。

调用栈

栈是一种遵循
LIFO(先进后出)规则的数据结构,用于存储和获取数据。JavaScript
引擎通过栈来记住一个函数中最后执行的语句所在的位置。

functionmultiplyByTwo(x){returnx*2;}functioncalculate(){constsum=4+2;returnmultiplyByTwo(sum);}calculate()varhello="somemorecodefollows"

1.引擎了解到我们的程序中有两个函数

2.运行 calculate() 函数

3.将 calculate 压栈并计算两数之和

4.运行 multiplyByTwo() 函数

5.将 multiplyByTwo 函数压栈并执行算术计算 x*2

6.在返回结果的同时,将 multiplyByTwo() 从栈中弹出,之后回到 calculate()
函数

7.在 calculate() 函数返回结果的同时,将 calculate()
从栈中弹出,继续执行后面的代码

栈溢出

在不对栈执行弹出的情况下,可连续压栈的数目取决于栈的大小。如果超过了这个界限之后还不断地压栈,最终会导致栈溢出。chrome
浏览器将会抛出一个错误以及被称为栈帧的栈快照。

递归:递归指的是函数调用自身。递归可以大幅度地减少执行算法所花费的时间(时间复杂度),不过它的理解和实施较为复杂。

下面的例子中,基本事件永远不会执行,lonley
函数在没有返回值的情况下不断地调用自身,最终会导致栈溢出。

functionlonely(){if(false){return1;//基本事件}lonely();//递归调用}

为什么 JavaScript 是单线程的?

一个线程代表着在同一时间段内可以单独执行的程序部分的数目。要想查看一门语言是单线程的还是多线程的,最简单的方式就是了解它有多少个调用栈。JS
只有一个,所以它是单线程语言。

这样不是会阻碍程序运行吗?如果我运行多个耗时的阻塞操作,例如 HTTP
请求,那么程序必须得在每一个操作得到响应之后才能执行后面的代码。

为了解决这个问题,我们需要找到一种可以在单线程下异步完成任务的办法。事件循环就是用来发挥这个作用的。

事件循环

到现在为止,我们谈到的内容大多包含在 V8 里面,但是如果你去查看 V8
的代码库,你会发现它并不包含例如 setTimeout 或者 DOM
的实现。事实上,除了运行引擎之外,JS 还包括浏览器提供的 Web API,这些
API 用于拓展 JS。

总结:

关于制作一门编程语言,其实还有很多内容,并且语言的实现在这些年也是不断变化的。我希望这两篇博客可以帮助你成为一名更好的
JS 程序员,并且接受 JS 中那些晦涩难懂的内容
。对于诸如“V8”,“事件循环”,“调用栈”这样的术语,你现在应该熟悉了。

大部分的学生(比如我)是从一个新的框架起步,之后再去学习原生
JS。现在他们应该熟悉代码背后发生的事情了,反过来,这将帮助他们写出更好的代码。

原文