对V8引擎的理解

222 阅读29分钟

JS引擎/V8引擎

js 引擎包括parser解析器、interperter解释器、 JIT compiler编译器、gc/garbage collector/垃圾回收器。

不同语言区别

静态语言、动态语言

  • 在使⽤之前需要确认变量的数据类型的语言称为静态语⾔,在声明变量前需要先定义变量类型。
  • 在运行过程中检查数据类型的语⾔称为动态语⾔,声明变量时并不需要确认其数据类型。

强类型、弱类型语言

  • 强类型语言:不⽀持隐式类型转换的语⾔。

    • 一旦变量被指定为某个数据类型,如果不经过强制转换/显式转换,那么它就永远是这个数据类型了。
    • 强类型语言带来的严谨性可以有效地帮助避免许多错误。
  • 弱类型语言:⽀持隐式类型转换的语⾔。

    • 变量类型可以忽略,可进行数据类型转换,如字符串'12'和整数3进行连接得到字符串'123'。
    • 在速度上快于强类型语言。

编译器与解释器

按语言的执行流程,可以把语言划分为编译型语言和解释型语言。因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。

由编译型语言编写的程序,在程序执行前需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如C/C++、GO等都是编译型语言。

由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如Python、JavaScript等都属于解释型语言。

编译型与解释型语言

  • 编译型语言:

    • 需要事先编译,将高级语言源代码一次性的编译成机器码,包装成平台能识别的可执行性程序,如exe格式的文件。只需编译一次,以后要再运行时直接使用编译结果,不需要编译;运行时脱离开发环境;所以执行效率高
    • 与特定平台相关,一般无法移植到其他平台
    • C、C++、Delphi、Pascal、Fortran等属于编译型语言。
  • 解释型语言:

    • 不需要事先编译,高级语言源代码每次执行时被解释器动态解释成机器码并立即执行,执行效率较低
    • 只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植,跨平台。
    • JavaScript、Java、Python等属于解释型语言。

c语言和js的区别

  • c需要编译成机器语言;JS是脚本语言有解释器执行。

  • JavaScript是⼀种动态、弱类型的语⾔。

    • 不需要告诉JavaScript引擎这个或那个变量是什么数据类型,JavaScript引擎在运行代码的时候自己会计算出来。
    • 可以使⽤同⼀个变量保存不同类型的数据。
  • C语言需要程序员手动管理内存,主要指堆内存的申请和释放;JavaScript的内存由垃圾回收器管理。

  • c言可以通过调用系统API来实现多线程,可以通过多线程来提高,阻塞操作如IO操作的CPU利用率。

    JavaScript主要是单线程,JavaScript的可能阻塞的操作都由JavaScript运行时提供的异步API来完成。

编译+执行流程

V8引擎如何执行一段JavaScript代码:当执行⼀段代码时,JavaScript引擎需要先编译,并创建执行上下⽂,然后再按照顺序执行代码。

  1. parser解析器将JavaScript源代码/高级语言转换为抽象语法树AST,并生成执行上下文。生成抽象语法树AST具体过程见下
  2. Ignition解释器将AST转换为字节码,并逐行解释执行字节码
  3. JIT compiler编译器编译执行时的热点代码,把字节码转成机器码,之后可以直接执行机器码。

编译阶段

⼀段代码经过编译后,⽣成执行上下文可执行代码/字节码

执行上下文:JavaScript引擎把声明的代码放在变量环境对象中。

可执行代码:JavaScript引擎会把声明以外的代码编译为字节码。

执行阶段

JavaScript引擎顺序执行代码,从变量环境中去查找⾃定义的变量和函数、进行赋值操作等。

编译阶段中执行上下文

执行上下文/Execution context是JavaScript执行⼀段代码时的运行环境,比如调用⼀个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如this、变量、对象以及函数等。

  • 只有理解了JavaScrip的执行上下文,才能更好地理解JavaScript语⾔本⾝,比如变量提升、作用域、闭包等。

执行上下文的种类

  • JavaScript执行全局代码时,会编译全局代码并创建全局执行上下文。在整个⻚⾯的⽣存周期内,全局执行上下文只有⼀份。
  • 调用⼀个函数时,函数体内的代码会被编译,并创建函数执行上下文。⼀般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  • 使用eval函数时,eval的代码也会被编译,并创建执行上下文。

全局执行上下文

全局作用域确定后,全局代码执行前, 将window对象确定为全局执行上下文对象,将其添加到栈中(压栈)。对全局数据进行预处理:

  • 全局作用域中var定义的全局变量==>赋值为undefined,添加为window的属性。变量的声明提升。
  • 全局作用域中function声明的全局函数==>赋值,添加为window的方法。函数的声明提升。先变量的声明提升,再函数的声明提升!!!。
  • this==>赋值为window
  • 开始执行全局代码,进行赋值操作
  • 当所有的代码执行完后, 栈中只剩下window对象。 当页面刷新/关闭页面时 window全局执行上下文死亡。

函数执行上下文

函数作用域确定后,调用函数时,函数体代码执行前,创建函数执行上下文对象,将其添加到栈中(压栈)。对局部数据进行预处理:

  • 形参变量==>赋值为实参,添加为函数执行上下文的属性。
  • arguments==>赋值为实参列表的伪数组,添加为函数执行上下文的属性。
  • 函数中var定义的局部变量==>赋值为undefined,添加为函数执行上下文的属性
  • 函数中function声明的函数 ==>赋值,添加为函数执行上下文的方法。先变量的声明提升,再函数的声明提升。
  • this==>赋值为调用函数的对象
  • 开始执行函数体代码,进行赋值操作
  • 在当前函数执行完后,将栈顶的对象移除(出栈)。

变量提升Hoisting

JavaScript代码执行过程中,JavaScript引擎把变量的声明部分函数的声明部分提升到代码开头的“行为”。变量被提升后,给变量设置为默认值undefined。

  • 语句分为声明、赋值。

    • 变量/函数表达式:声明、赋值。
    • 具名函数:完整的函数声明
  • 变量提升导致函数中的变量⽆论是在哪⾥声明的,在编译阶段都会被提取到执行上下文的变量环境中,这些变量在整个函数体内部任何地⽅都能访问,导致很多与直觉不符的代码。变量提升带来后果:

    • 变量容易在不被察觉的情况下被覆盖掉,变量作用范围是整个函数,任何赋值操作都会直接改变变量环境中变量的值。
    • 本应销毁的变量没有被销毁。在创建执行上下文阶段,变量i就已经被提升了,当for循环结束之后,变量i并没有被销毁。
  • 解决办法:ECMAScript6/ES6通过引⼊let、const关键字实现块级作用域,避开这种设计缺陷。

变量环境和词法环境

JavaScript引擎同时⽀持变量提升和块级作用域。JavaScript引擎通过变量环境实现函数级作用域/变量提升,ES6通过词法环境的栈结构实现块级作用域

  • 执行上下文中存在变量环境的对象/Viriable Environment,该对象中保存了变量提升的内容

  • 执行上下文中存在词法环境的对象。通过let或者const声明的变量放在词法环境中。

    • 词法环境内部维护了⼀个⼩型栈结构,栈底是函数最外层的变量,进⼊⼀个块级作用域后,就会把该块级作用域内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出。
    • 块级作用域外声明了某个变量,块级作用域内部也声明了相同名字的变量,它们是独⽴的存在,不会相互影响。

寻找变量的终极顺序

  1. 函数编译,并创建执行上下文。

    • 通过var声明的变量,编译阶段全都被存放到变量环境⾥。
    • 通过let声明的变量,在编译阶段会被存放到词法环境里。
    • 函数的块级作用域内部,通过let声明的变量并没有被存放到词法环境中。
  2. 执行代码。

  3. 进⼊函数的块级作用域,块级作用域中通过let声明的变量,存放在词法环境的⼀个单独区域中。

  4. 执行代码。

    • 当前执行上下文中查找变量:顺序是沿着词法环境的栈顶向下查询,再到变量环境中查找。
    • 外部嵌套函数产生的闭包。
    • outer指向的外部执行上下文中查找变量:顺序同上。
  5. 块级作用域执行结束后,其内部定义的变量就会从词法环境的栈顶弹出。

调用栈

在执行JavaScript时可能会存在多个执行上下文,执行上下文创建好后,JavaScript引擎通过栈的数据结构来管理这些执行上下文,把执行上下文压入栈中,把这种用来管理函数调用关系的栈称为执行上下文栈/调用栈

  • 通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

  • 某行代码加上断点,刷新⻚⾯。依次执行,断点处流程暂停,通过右边“call stack”来查看当前的调用栈的情况

    一般情况下,栈的最底部是anonymous/全局的函数入⼝。

  • 使用console.trace()来输出当前的函数调用关系。

函数调用

函数调用就是运行⼀个函数,具体使用⽅式是使用函数名称跟着⼀对小括号。

  • 函数加括号,代表函数调用,相当于使用函数的返回值;

    函数不加括号,代表函数对象,相当于直接使用函数对象。

  • 每次调用函数,浏览器都会传递两个隐含的参数:调用函数的当前对象this、封装实参的类数组arguments。

    this指向的是 调用函数的当前对象。任何函数本质上都是通过某个对象来调用的!! ,如果没有直接指定那就是window。

过程

  1. 每调用⼀个函数,JavaScript引擎从全局执行上下文中取出该函数代码。
  2. 对函数代码进行编译,并创建该函数的执行上下文、可执行代码。
  3. 把该执行上下文压入调用栈
  4. JavaScript引擎执行函数代码,输出结果。
  5. 当前函数执行完毕后,JavaScript引擎会将该函数的执行上下文弹出栈。
  6. 递归时会多次入栈、出栈。

栈溢出

调用栈是有大小的,当入栈的执行上下文超过⼀定数⽬,JavaScript引擎就会报错,这种错误叫做栈溢出/Stack Overflow。

  • 特别是写递归代码时,就很容易出现栈溢出的情况。

    函数递归如果没有任何终⽌条件,则会⼀直创建新的函数执行上下文,并反复将其压入栈中,超过栈的容量限制后就会栈溢出。

    解决办法:递归调用的形式改为循环形式、使用加入定时器的⽅法来把当前任务拆分为其他很多小任务、尾递归优化??。

作用域scope

作用域是在程序中定义变量的区域,该位置决定了变量的⽣命周期/作用范围。

  • 作用是隔离变量,可以在不同作用域定义同名的变量不冲突。作用块内声明的变量不影响块外⾯的变量。

作用域类型

  • 全局作用域:全局作用域中的对象在代码中的任何地⽅都能访问,其⽣命周期伴随着⻚⾯的⽣命周期。

  • 函数作用域:在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

  • 块级作用域:

    • 块级作用域是使用⼀对⼤括号包裹的⼀段代码,比如函数、判断语句、循环语句,甚⾄单独的⼀个{}都可以被看作是⼀个块级作用域。如函数块、if块、for循环块、while块、{}单独一个块等。
    • 如果⼀种语⾔⽀持块级作用域,那么其代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁。

全局作用域

  • 直接编写在script标签中的JS代码都在全局作用域。最外层函数外面定义的变量,拥有全局作用域。

  • 在全局作用域中有一个全局对象window,该对象由浏览器创建,代表浏览器窗口,我们可以直接使用。

    全局作用域中创建的变量都会作为window对象的属性保存;使用变量时可以直接通过变量名使用,也可以window.变量名使用。

    全局作用域中创建的函数都会作为window对象的方法保存。调用函数时可以直接通过函数名()调用,也可以window.函数名()调用。

  • 所有未定义直接赋值的变量自动声明为全局作用域。

  • 全局作用域中的变量都是全局变量,在页面的任意位置都可以访问的到。全局作用域有很大的弊端,过多的全局变量会污染全局命名空间,容易引起命名冲突。

函数作用域

  • 全局作用域内部,每个函数都会创建自己的作用域。

    函数作用域和全局作用域很多性质相似,函数作用域就是小的全局作用域。

  • 在函数作用域中可以访问到全局作用域的变量,访问全局变量可使用window对象;在全局作用域中无法访问到函数作用域的变量。

  • 当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用;

    如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错ReferenceError。

块作用域

  • 使用let和const指令,声明块级作用域。
  • 花括号内代码。
  • 块级作用域适合于循环中,这样就可以把声明的计数器变量限制在循环内部。

作用域链

查找变量时,JavaScript引擎⾸先会在“当前的执行上下文”中查找该变量,如果在当前的变量环境中没有查找到,那么JavaScript引擎会继续在outer所指向的执行上下文中查找,依次向上查找,直到访问到全局执行上下文就终止。这个查找的链条就称为作用域链

  • 在每个执行上下文的变量环境中都包含了⼀个外部引用outer,用来指向外部的执行上下文。

  • JavaScript作用域链由词法作用域决定,词法作用域是静态的作用域

    作用域链在函数声明时就已经决定,和函数嵌套调用没有关系。

  • 函数任意地⽅打上断点,然后刷新⻚⾯,右边Scope项就体现出了作用域链的情况。

    Local是当前函数的作用域,Closure是外部嵌套函数产生的闭包,最下⾯Global是全局作用域。

    Local当前函数作用域 => Closure闭包 => Global全局作用域是完整的作用域链

本质

一个指向变量对象的指针列表

  • 词法环境对象 + 变量环境对象中包含了执行环境中所有变量、函数。
  • 作用域链的始终都是当前执行上下文的词法环境对象 + 变量环境对象,作用域链的始终是全局执行上下文的词法环境对象 + 变量环境对象。

作用

  • 有序访问执行环境中有权限的所有变量、函数;
  • 通过作用域链可以访问到外层环境的变量和函数。

作用域与执行上下文

  • 作用域是静态的,在函数定义时就已经确定了,一直存在就不会再变化,也只有一个。
  • 执行上下文是动态的,调用函数时才创建执行上下文,如果调用函数多次,可创建多个执行上下文。函数调用结束时就会自动释放。
  • 作用域本来就存在,执行上下文环境在对应的作用域中生成。全局上下文环境从属于全局作用域,函数上下文环境从属于对应的函数作用域。

闭包closure

在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用⼀个外部函数返回⼀个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

  • 闭包是内部函数中的一个对象,对象中包含引用的外部函数的数据。通过chrome调试台查看得到的。

产生条件

  1. 函数嵌套

  2. 内部函数引用了外部函数的数据如变量、函数。

  3. 调用外部函数,内部函数声明。

    • 不同的内部函数产生不同的闭包。
    • 每次调用外部函数,都会产生闭包。

闭包的生命周期

  • 产生:闭包在嵌套的内部函数声明时产生。如果函数存在声明提升就会在提前的位置产生,没有函数声明提升就在函数声明时产生。

    内部函数引⽤了外部函数的变量,所以JavaScript引擎判断这是⼀个闭包,于是在堆空间创建⼀个“closure”的闭包对象,⽤来保存这些变量。

  • 死亡:包含闭包的内部函数对象成为垃圾对象时释放闭包。有的内部函数对象自动成为垃圾对象,有时需手动让其成为垃圾对象。

闭包优点/作用

  • 保护:避免全局变量的污染

  • 函数外部默认不能直接访问函数内部的局部变量,但可以通过闭包实现。

    函数外部可以通过闭包读写函数内的局部变量,同一个闭包多次执行则多次改变函数内的局部变量。

  • 函数执行完后,函数对象内部的局部变量一般被释放。

    保存:延长局部变量的生命周期。包含闭包的内部函数对象在手动设置为垃圾对象前,闭包中局部变量不会被释放,可以让一个变量长期存储在内存中。

闭包缺点/解决办法

缺点:

  • 常驻内存,增加内存使用量。
  • 如果没有及时手动释放闭包,可能导致内存泄露。

解决办法:

  • 尽量不使用,减少使用。
  • 及时手动让包含闭包的内部函数对象成为垃圾对象,释放闭包,函数中局部变量也会在栈内存中释放。如f = null让内部函数对象成为垃圾对象。

常见闭包/闭包的应用场景

  • 函数作为返回值。将函数3作为函数1的返回值,函数1向全局暴露一个函数3。

  • 将函数作为某个函数的参数调用。如将匿名函数 作为定时器的第一个参数。

  • 循环遍历加监听。自执行函数/立即执行函数。

  • 函数柯里化。传入的参数会做存储留用,等到参数传完了,再对参数做处理。

  • 实现简单的缓存工具,封装对象的私有属性和方法,只提供API,实现隐藏数据。如JS框架(jQuery)大量使用了闭包。

    模块化: 封装一些数据以及操作数据的函数, 向外暴露一些行为。

    模块化中的函数数据是私有的,全局中默认不可操作。但通过闭包可以在全局中操作 函数内的私有数据。

  • 单例模式??

  • 函数防抖、节流??

this机制

在对象内部的⽅法中使用对象内部的属性是⼀个⾮常普遍的需求。但是JavaScript的作用域机制并不⽀持这⼀点,基于这个需求,JavaScript⼜搞出来另外⼀套this机制。

  • 为什么要有this?基于上面需求,指向当前代码运行时所处的上下文环境context。

  • 作用域链和this是两套不同的系统,它们之间基本没太多联系。明确这点,防止和作用域产⽣⼀些不必要的关联。

  • 每个执行上下文中都包含四部分:变量环境、词法环境、外部环境outer、this。

    this是和执行上下文绑定的,每个执行上下文中都有⼀个this。

this的种类

执行上下文主要分为三种:全局执行上下文、函数执行上下文、eval执行上下文,所以对应的this也是这三种:全局执行上下文中的this、函数中 的this、eval中的this。

全局执行上下文中的this

全局执行上下文中的this指向window对象。

函数执行上下文中的this

  • 开发者主动函数调用的情况:

    • 默认情况下调用⼀个普通函数,函数执行上下文中的this指向window对象
    • 通过函数的apply()、call()方法时,如fun.call/apply(obj),并没有直接调用fun函数,⽽是调用了fun的call⽅法,此时临时让fun成为obj的方法进行调用,函数执行上下文中的this指向指定的obj对象。js可以让任何函数 成为对象的方法 进行调用。
    • 对象调用对象方法时,函数中this是该对象
    • 以new 构造函数的形式调用时,构造函数中this 是新创建的实例对象
  • 回调函数调用的情况:

  • 看背后是通过谁来调用的,如window、其它对象。

this缺陷

  • 嵌套函数中的this不会从外层函数中继承。解决办法如下:

    • 把this保存为⼀个self变量,再利用变量的作用域机制传递给嵌套函数。
    • 把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承外部函数中的this。
  • 普通函数中执行上下文中的this默认指向全局对象window,这样会打破数据的边界,造成⼀些误操作。

    解决办法是设置JavaScript的“严格模式”,函数的执行上下文中的this默认是undefined。

内存机制

JavaScript中的数据是如何存储在内存中。

存储空间的种类

  • 代码空间:存储可执行代码。

  • 栈内存:即调⽤栈,存储执行上下⽂。执行上下⽂包含变量环境、词法环境、外部环境outer、this机制、可执行代码。

    • 变量名标识这块内存;变量值指的是栈内存中的数据,即基本数据类型的值、对象数据类型的地址值。
    • 栈空间都不会设置太⼤,主要⽤来存放⼀些原始类型的⼩数据。数据被频繁使用。
    • 原始类型的数据值都是直接保存在“栈”中的。
    • JavaScript引擎用栈空间维护程序执行期间上下⽂的状态,栈空间一般较小,太大影响到上下文切换的效率,进⽽影响整个程序的执行效率。
    • 函数执行结束,JavaScript引擎需要离开当前的执行上下⽂,指针下移,函数执行上下⽂栈区空间全部回收。
    • 栈速度比较快
    • 大小固定?
    • 栈是连续的空间?
    • 栈由系统自动分配?
  • 堆内存:

    • 地址标识这块内存;存放对象。
    • 堆空间很⼤,能存放很多⼤的数据。
    • 引⽤类型的值是存放在“堆”中的。引⽤类型的数据占⽤的空间都比较⼤,这⼀类数据会被存放到堆中。
    • 堆比较慢
    • 大小不固定?
    • 堆是不连续的空间?
    • 堆是自己申请开辟?

内存泄露与内存溢出

内存泄露Memory leak

占用的内存没有及时释放,内存泄露积累多了就容易导致内存溢出。

常见场景
  • 意外的全局变量。使用严格模式可以避免。
  • 没有及时清理的回调函数。如定时器函数、事件监听回调函数
  • 引用DOM元素
  • 闭包
如何判断JS中内存泄漏
  • 感官上的⻓时间运行⻚⾯卡顿,猜可能会有内存泄漏。
  • 通过DynaTrace、profiles等工具收集⼀段时间数据,观察对象使用情况,判断是否存在内存泄漏。
避免内存泄漏的方法
  • 确定不使用的对象置为null
  • 减少使用闭包

内存溢出

内存溢出是一种程序运行出现的错误,当程序运行需要的内存超过了剩余的栈内存时, 就出抛出内存溢出的错误。内存溢出是栈溢出。

常见场景
  • 无限递归、递归层级过深
  • 内存泄漏导致内存溢出

垃圾回收机制/策略

后续不再使用的数据称为垃圾数据,需要对这些垃圾数据进行回收,以释放有限的内存空间。如果没有及时释放内存称为内存泄漏

不同语言的垃圾回收策略

  • ⼿动回收:如C/C++语言,分配内存、销毁内存由代码控制。手动调用mallco函数分配内存,⼿动调用free函数释放内存。

  • ⾃动回收:如Java、JavaScript、Python等高级语⾔,销毁内存由垃圾回收器自动释放,自动将垃圾对象从内存中清理。

    对于不再使用的对象,开发者需要手动将变量设置为null,无需手动释放内存。没有任何的变量对对象引用,则为垃圾对象,垃圾回收器则发挥作用。

栈中回收垃圾数据

因为原始数据类型是存储在栈空间中,引用类型的数据存储在堆空间中,需要分别处理两者中垃圾数据,释放内存包括栈内存和堆内存

  1. 函数执行时,执行上下文压入到调用栈中。

    记录当前执行状态的指针ESP,指向调用栈中函数的执行上下文,表⽰当前正在执行该函数。

  2. 函数执行结束,执行上下文在栈中被销毁。

    JavaScript引擎向下移动ESP指针到其他函数的执行上下文如全局执行上下文,销毁该函数在栈中的执行上下文,立即释放栈内存。

  3. 全局变量在关闭内存时自动释放。

堆中回收垃圾数据

栈中释放栈内存,但对象仍占据堆空间。要回收堆中的垃圾数据,就需要用到JavaScript中的垃圾回收器

JS引擎中的垃圾回收器/garbage collector/gc:作用是清理垃圾对象。对象变成垃圾对象后,垃圾回收器一段时间间隔后自动回收。

代际假说

The Generational Hypothesis

  • 大部分对象在内存中存在的时间很短。大部分对象⼀经分配内存,很快就变成不可访问;
  • 不死的对象活得较久。
  • 这两个特点不仅仅适用于JavaScript,同样适用于大多数的动态语⾔如Java、Python等。

垃圾回收器的工作流程

  1. 标记阶段。标记空间中活动对象非活动对象。活动对象就是还在使用的对象,非活动对象是可以进行垃圾回收的对象。

  2. 垃圾清理。回收非活动对象所占内存,统⼀清理内存中所有被标记为可回收的对象。

  3. 内存整理。可选,副垃圾回收器不会产生内存碎片。

    频繁回收对象后,内存中就会存在大量不连续空间,即内存碎片。当内存中出现了大量内存碎片,如果需要分配较大连续内存的时候,就有可能出现内存不⾜的情况,所以需要整理内存碎片。

新生代

  • 新生区容量小,通常是1~8M。

  • 存放小的对象、生存时间短的对象。

  • 副垃圾回收器负责新生区的垃圾回收。垃圾回收比较频繁,采用Scavenge算法

    1. 把新生代空间对半划分为两个区域,⼀半是对象区域,⼀半是空闲区域。

    2. 新加入的对象存放到对象区域,当对象区域快被写满时,执行⼀次垃圾清理操作。

    3. 标记阶段。标记对象区域中活动对象和非活动对象。

    4. 垃圾清理。副垃圾回收器会把这些存活的对象复制到空闲区域中,并有序排列

      这个复制过程相当于完成了内存整理操作,空闲区域不会产生内存碎片。

      复制操作需要时间成本,如果新生区空间设置得太大,那么每次清理的时间过久,所以为了执行效率,⼀般新生区的空间会被设置得比较小。

    5. 对象区域与空闲区域进行⻆⾊翻转,⻆⾊翻转的操作还能让新生代中的这两块区域⽆限重复使用下去。

    6. JavaScript引擎采用对象晋升策略,经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

      因为新生区的空间不大,所以很容易被存活的对象装满整个区域,所以经常使用的对象晋升到老生区。

老生代

  • 老生区容量大。

  • 存放大的对象、生存时间久的对象、新生区中晋升的对象。

  • 主垃圾回收器负责老生区的垃圾回收。

    标记-清除Mark-Sweep算法

    1. 标记阶段。遍历调用栈,若根据地址可找到对应对象,则标记为活动对象;未对应地址的对象为非活动对象。
    2. 垃圾清理。直接清理非活动对象所占内存,可能产生大量不连续的内存碎片。

    标记-整理Mark-Compact算法

    1. 标记阶段。同上。
    2. 整理阶段。所有存活对象向⼀端移动,直接清理掉端边界以外的内存。

全停顿Stop-The-World

JavaScript运行在主线程上,⼀旦执行垃圾回收算法,则正在执行的JavaScript脚本需要暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿

由于垃圾回收引起JavaScript线程暂停执行,若是时间花销大,那么应用的性能和响应能⼒都会直线下降。

  • 在V8新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿影响不大,

  • 老生代占用主线程时间过久,造成⻚⾯的卡顿。

    为了降低老生代的垃圾回收⽽造成的卡顿,V8将标记过程分为⼀个个的⼦标记过程,让垃圾回收标记和JavaScript应用逻辑交替进行,直到标记阶段完成。

    增量-标记Incremental-Marking算法:把完整的垃圾回收任务拆分为很多小任务,小任务执行时间短,穿插在其他的JavaScript任务中间执行。