一、内存机制

1.1 js语言类型

JavaScript 是一种弱类型的、动态的语言。

  • 弱类型:它支持隐式类型转换
  • 动态:它在运行过程中才需要检查数据类型

1.2 js数据类型

基础类型:boolean、null、undefined、number、string、symbol
引用类型:object

原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址

1.3 内存空间

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

栈空间:

  • 栈空间就是调用栈,是用来存储执行上下文的空间
  • 基础类型的数据值都会保存在栈中
  • 栈空间不大,用于存放小数据

堆空间:

  • 一块内存地址
  • 引用类型的值存放在堆中
  • 堆空间很大,能存放很多数据

区分栈、堆空间的原因:如果所有的数据都存放在栈空间,会影响上下文切换的效率,进而影响整个程序的执行效率

二、垃圾回收

javascript采用的是自动垃圾回收的策略,产生的垃圾数据是由垃圾回收器进行释放

2.1 栈中的垃圾回收

栈中有一个用于记录当前执行状态的指针(称为 ESP),当函数执行完需要出栈时,js会将ESP指针指向下一个需要执行的函数上下文中,这个下移操作就是销毁该函数保存在栈中的执行上下文

2.2 堆中的垃圾回收

了解堆中的垃圾回收前,需要了解两个概念:代际假说、分代收集

  • 代际假说

    • 大部分对象在内存中存在的时间很短,很多对象一经分配内存,很快就变得不可访问;
    • 不死的对象,会活得更久
  • 分代收集

    在V8 中会把堆分为新生代老生代两个区域,

  • 新生代

    • 存放的是生存时间短的对象
    • 空间较小,只有1-8M
    • 副垃圾回收器进行垃圾回收
  • 老生代
    • 存放的生存时间久的对象
    • 容量大
    • 主垃圾回收器进行垃圾回收

2.3 垃圾回收工作流程

  • 标记空间中活动对象非活动对象
    • 活动对象指还在使用的对象
    • 非活动对象指可以进行垃圾回收的对象
  • 回收非活动对象所占据的内存
    • 在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象
  • 内存整理
    • 将不连续的内存空间(内存碎片)整理成连续的内存(这一步是可选的)

2.4 副垃圾回收器(新生代区域)回收流程

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

新加入的对象都会存放到对象区域,当对象区域快被写满时,执行以下回收流程:

  • 对对象区域中的垃圾做标记
  • 将存活的对象复制到空闲区域中,同时还会把这些对象有序地排列起来(复制+内存整理)
  • 对象区域与空闲区域进行角色翻转:也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域

优点:

  • 角色翻转的操作还能让新生代中的这两块区域无限重复使用下去

缺点:

  • 执行清理时,都需要复制一次内容,需要时间成本
    • 解决:新生代区域内存都比较小
  • 因为区域较小,所以很容易就被填满
    • 解决:对象晋升策略:经过两次垃圾回收依然还存活的对象,会被移动到老生代区中

2.5 主垃圾回收器(老生代区域)回收流程

老生区中的对象有两个特点:对象占用空间大、对象存活时间长
主垃圾回收器是采用标记 - 清除 算法进行垃圾回收

回收的流程:

  • 标记阶段:遍历整个调用栈,如果没有找到对应堆中的内存地址的引用时,该地址就被标记为垃圾元素,如果找到对应堆的内存地址时,就被标记为活动对象
  • 清除阶段:将标记的垃圾元素,从内存地址上删除

缺点:删除时会产生很多不连续的内存碎片

于是继续引入标记 - 整理 算法进行内存整理

整理的流程:

  • 标记阶段:同回收流程的标记阶段,同时标记出垃圾元素、活动对象
  • 整理阶段:让所有活动对象都向一端移动,然后直接清理掉端边界以外的内存

2.6 JavaScript运行时垃圾回收机制

全停顿:由于 JavaScript 是运行在主线程之上,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行

具体影响:
新生代区域:区域较小,执行较快,对js运行影响不大
老生代区域:区域较大,执行时间较为长,很容易阻塞js运行

为了解决老生代区域垃圾回收卡顿问题于是引入:增量-标记 算法:

原理:把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,降低单次垃圾回收执行时间

三、执行JavaScript机制

JavaScript 属于解释型语言,在每次运行时都需要通过解释器对程序进行动态解释和执行

具体执行流程:

image.png

3.1 生成抽象语法树(AST)

具体阶段:

  • 词法分析阶段:将一行行的源码拆解成一个个语法上不可能再分的、最小的单个字符或字符串 ```javascript let name = ‘gelx’

以上代码会被拆分为:

let —> 关键词 name —> 标识符 = —> 赋值 ‘gelx’ —> 字符串 ```

  • 语法分析阶段:其作用是将上一步生成的 token 数据,根据语法规则转为 AST

3.2 生成字节码

有了以上AST后,解释器会根据AST生成字节码

字节码是介于 AST 和机器码之间的一种代码,与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行,它能够解决内存占用过多的问题

3.3 执行代码

执行流程如下:

  • 判断该字节码是否是第一次被执行
  • 第一次被执行的代码,解释器逐条解释执行
  • 多次被执行的代码:标记为热点代码,编译器会把该字节码编译为高效的机器码,再次执行这段代码时,只需要执行机器码即可,(省去翻译为字节码的过程),大大提升代码的执行效率