?【评论拿徽章】你需要知道的JavaScript V8引擎工作原理

前言

对于前端开发者来说,JavaScript V8引擎是一个经常不被提及的概念,因此很容易被忽视,特别是一些非计算机专业的同学,对V8引擎可能没有非常清晰的认识,甚至有些同学根本不知道V8引擎是什么。

如果你想成为前端精英,那么你就必须要搞清楚V8引擎的工作原理,它将帮助你打造高性能的前端应用,让你更加了解浏览器以及JS这门语言。

这篇文章将从数据存储,垃圾回收两个方面带你深入JS V8引擎,快来学习吧,如有错误的地方,还望指正,此外文末还有思考题,参与评论都有机会获得徽章喔~

栈空间和堆空间

这一部分你需要弄清楚JavaScript的内存机制,即数据是如何存储在内存中的。

首先我们看下面两段代码:

function foo1(){
    var a = 1
    var b = a
    a = 2
    console.log(a)
    console.log(b)
}
foo1()
复制代码
function foo2(){
    var a = {name:"橙子"}
    var b = a
    a.name = "柚子" 
    console.log(a)
    console.log(b)
}
foo2()
复制代码

执行第一段代码后,打印a的值是2,b的值是1,这没有什么难理解的。

执行第二段代码后,打印a和b的值都为{name:"橙子"},是不是很奇怪,要弄清楚这个问题,我们得知道JavaScript的数据类型。

JS的数据类型

其实JavaScript中的数据类型一共有8种,它们分别是:

JS数据类型.png

我们把前面7种数据类型称为原始类型,把最后一个对象类型称为引用类型,之所以把它们区分为两种不同的类型,是因为它们在内存中存放的位置不一样。接下来,我们就来讲解一下JavaScript的原始类型和引用类型到底是怎么存储的。

内存空间

要理解JavaScript在运行过程中数据是如何存储的,你就得搞清楚其存储空间的种类。

JavaScript执行过程中,主要有三种类型内存空间,分别是代码空间栈空间堆空间

代码空间主要是存储可执行代码的,我们为了解决上面的问题就来说说栈空间和堆空间。

栈空间和堆空间

这里的栈空间就是调用栈,是用来存储执行上下文的。

我们看下面这段代码:

function foo(){
    var a = "橙子"
    var b = a
    var c = {name:"橙子"}
    var d = c
}
foo()
复制代码

分析这段当执行到第3行时,变量a和变量b的值都保存在执行上下文中,而执行上下文又被压入到中,因而你也可以认为变量a和变量b的值都是存放在中的。

然后执行的第4行代码,由于JavaScript引擎判断右边的值是一个引用类型,这时候JavaScript引擎会把它分配到堆空间里,分配后该对象会有一个在“堆”中的地址,然后把该地址写进c的变量值。

现在我们已经清楚了原始类型的数据值都是直接保存在“栈”中,引用类型的值是存放在“堆”中的,你也许会问为什么不把所有的数据直接存放在“栈”中呢?

然而这是不可以的,因为JavaScript引擎需要栈来维护程序执行期间上下文的状态,如果所有数据都
存放在栈空间里,会影响到上下文切换的效率,进而又影响到整个程序的执行效率。

  • 栈空间不会设置太大,主要用来存放原始类型的小数据。
  • 堆空间很大,能存放很大的数据。

如何赋值?

在JavaScript中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复值引用地址

如何实现深拷贝?

在实际的项目中,经常需要完整地拷贝一个对象,也就是拷贝完成之后两个对象之间不能相互影响呢,我这里给出一种实现方式,大家可以借鉴一下~

function deepClone(obj) {
  // obj是null或者不是对象和数组直接返回
  if (typeof obj !== 'object'|| obj == null) {
    return obj
  }
  // 初始化返回结果
  let result
  if (obj instanceof Array) {
    result = []
  } else {
    result = {}
  }
  for (let key in obj) {
    // 保证key不是原型上的属性
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key])
    }
  }
  return result
}
复制代码

垃圾回收

在程序中,有些数据被使用之后,可能就不再需要了,我们把这种数据称为垃圾数据,如果垃圾数据一直保存在内存中那么内存会越用越多,所以我们需要对这些数据进行垃圾回收,以释放有限地内存空间

不同语言的垃圾回收策略

通常情况下,垃圾数据回收分为手动回收自动回收两种策略。

如C/C++就是使用手动回收策略,何时分配内存、何时销毁内存都是由代码控制的,在C中,我们需要调用mallco函数分配内存空间,然后再使用;当不再需要这块数据的时候,就要手动调用free函数来释放内存,如果没有主动调用free函数来销毁,这种情况就被称为内存泄漏

JavaScript、Java、Python等语言都会采用自动垃圾回收的策略,产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放。

接下来我们就来谈谈在JS中,“栈中的垃圾数据”和“堆中的垃圾数据”时如何回收的。看下面这段代码:

调用栈中的数据是如何回收的


function foo(){
    var a = 1
    var b = {name:"橙子"}
    function showName(){
      var c = 2
      var d = {name:"柚子"}
    }
    showName()
}
foo()
复制代码

执行这段代码,原始类型的数据会被分配到栈中,引用类型会被分配到堆中,当foo函数执行结束之后,foo函数的执行上下文会从堆中被销毁掉,它是怎么被销毁的呢?

销毁操作是靠ESP(记录当前执行状态的指针)实现的,当showName函数执行完成后,于是函数执行流程进入了foo函数,这个时候ESP指针也就从showName函数执行上下文下移到foo函数的执行上下文,这个下移操作就是销毁了showName函数执行上下文的过程。

堆中的数据是如何回收的

要回收堆中的垃圾数据,就需要用到JavaScript中的垃圾回收器了

代际假说和分代收集

在将V8如何实现回收之前,需要学习一下代际假说的内容,这是垃圾回收邻域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础上,所以非常重要。

代际假说有两个特点:

  1. 大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问。

  2. 第二个是不死的对象,会活的更久。

垃圾回收算法有很多,你需要权衡各种场景,根据对象的生存周期的不同而使用不同的算法,以便达到最好的效果。

在V8这会把堆分为新生代老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

V8分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收:

  • 副垃圾回收器:负责新生代区域的垃圾回收。
  • 主垃圾回收器:负责老生代的垃圾回收。

垃圾回收器的工作流程

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

  2. 回收非活动对象所占据的内存。

  3. 内存整理,频繁回收对象后,内存中就会存在大量不连续空间,也称其为内存碎片。当内存中出现大量内存碎片后,如果需要较大连续内存的时候,有可能会出现内存不足的情况,所以需要内存整理,当然这一步也是可选的,副垃圾回收器并不会产生内存碎片。

我们就按照这个流程来分析副垃圾回收器和主垃圾回收器是如何处理垃圾回收的。

副垃圾回收器

大多数小的对象会被分配到新生代区域,这个区域垃圾回收还是比较频繁的。

新生代中用Scavenge算法来处理,把新生代空间对半划分为两个区域,一半是对象区域,另一半是空闲区域,如下图:

image.png

新写入的对象会被存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。

  1. 标记阶段
  2. 垃圾清理阶段,副垃圾回收器会把存活的对象复制到空闲区域中。
  3. 内存整理阶段,把这些对象有序地排列起来
  4. 翻转阶段,对象区域空闲区域翻转

这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中这两块区域无限重复下去。

Scavenge算法中,为了执行效率,一般新生区空间会被设置得比较小。

由于新生区空间不大,存活的对象容易装满整个区域,JS引擎为了解决这个问题采用了对象晋升策略,经过两次垃圾回收依然存活的对象,会被移动到老生区。

主垃圾回收器

主垃圾回收器采用标记-清除算法进行垃圾回收的

  1. 标记过程阶段,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素可以判断为垃圾数据

  2. 清除阶段,清除掉被标记的垃圾数据

由于标记-清除算法对一块内存多次执行标记-清除算法后,会产生大量不连续的内存碎片。为了解决这个问题又产生了标记-整理算法,这个步骤和标记-清除算法类似,只是后续步骤上不一样,标记-整理算法是让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

增量标记算法

由于JavaScript是运行在主线程之上的,一旦执行垃圾回收算法,然后需要将JavaScript脚本暂停下来,待垃圾回收完毕后再恢复脚本执行,我们把这种行为叫做全停顿。这样非常容易造成页面的卡顿,为了降低老生代垃圾回收而造成的卡顿,出现了增量标记算法,V8将标记过程分为一个个的子标记过程,同时让子标记过程和JavaScript应用逻辑交替,直到标记阶段完成。

image.png
使用增量标记算法,把整个垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,穿插在其他JavaScript任务间执行,页面就不会变卡顿啦~

参考资料:
《JavaScript高级程序设计(第四版)》

极客时间《浏览器工作原理与实践》

思考题(抽奖)环节

到这,相信你已经对JS V8引擎有了一定的了解,给大家留一道思考题~欢迎在评论区分享你的想法?‍♂️,我会从评论区随机抽取两位幸运儿各送出掘金徽章一枚(掘金官方承担),来评论区一起思考一起记录吧!

思考题:如何判断JavaScript内存泄漏的?在开发过程中又如何避免内存泄露的问题呢?

最后

⚽这篇文章主要结合了数据存储以及垃圾回收的知识带你了解了JS V8引擎的工作原理,相信你一定收获不少~
⚾如果你对这篇文章感兴趣欢迎点赞关注+收藏,更多精彩知识正在等你!?
?GitHub 博客地址: github.com/Awu1227
?笔者还有其他专栏,欢迎阅读~
?玩转CSS之美
?Vue从放弃到入门
?深入浅出JavaScript

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享