浏览器原理与实现-学习3-堆和栈/垃圾回收/编译器解析器

175 阅读9分钟

1.堆和栈

语言差异

  • 静态语言
    • 必须提前确定变量的类型,Integer a = 10; String b = "str"
    • c,java,c#
  • 动态语言
    • 在运行的时候动态检查类型 var a = 10 ; var b = "str"
    • javascript,python
  • 强类型语言
    • 在运行时候不能把 动态改变原有定义的类型 如 int a =1; a = 'bb'
    • java,c#
  • 弱类型语言
    • 在运行时候可以把 动态改变原有定义的类型 如 int a =1; a = 'bb' 也叫隐式转换
    • javascript, c 

js 8种数据类型

7基本数据类型

  • Boolean
    • true
    • false
  • Number
    • 双精度64位
  • Null
    • null
  • Undefined
    • undefined 变量提升时,使用的就是 undefined
  • BigInt
    • 支持任意精度
  • String
    • 文本数据,js文本是不可变的
  • Symbol
    • 符号类型是唯一的,不可变的,多用于系统级别的 key 

1种引用类型

  • Object:一组属性的集合
  • object存放的是上面7种数据类型的集合,由key-value组成

  • typeOf null 返回  Object, 实际应该是Null 历史问题,未修复

内存空间

内存空间包含:代码空间 + 栈空间 + 堆空间

  • 代码空间
    • 存储可执行的代码
  • 栈空间
    • 原始数据类型存储在栈,因为原始数据内容长度是确定的,也不大
    • 复制原始数据类型:在栈中是复制完整的内容,内容之间完全独立,不互相影响
    • 调用栈,储存执行的上下文
  • 堆空间
    • Object 引用类型存储在堆,因为堆空间大,如果放在栈,会影响不同栈执行切换的效率
    • 复制对象:只是复制对象的引用,内容都是同一份

例子

function foo(){
    var a = "jason"
    var b = a
    var c = {name:"sinky"}
    var d = c 
}
foo()

foo函数执行上下文就是一个栈,栈中存储了 a,b,c,d ,其中a和b是原始类型字符串'jason' 存放在栈, 变量c和d是引用类型。只在栈中存储0x0001 地址,不存储具体的值。每次访问都动态再去堆里找内容。

image.png

如果把堆直接放在栈中,会影响整个栈中上下文的切换。

如foo执行上下文执行完销毁时,回到全局执行上下文的,如果栈过大会很慢。

当修改引用对象的值时,也是修改堆中的数据,栈中并未变化

function foo(){
    var a = "jason"
    var b = a
    var c = {name:"sinky"}
    var d = c
    c.name = 'sinky2' //这里修改的是堆中的数据
}
foo()

在内存中的分析闭包

在函数运行时,系统快速扫描代码,如果满足闭包条件,会创建对象,并把闭包的数据放到堆里。即函数运行时,闭包的栈堆就提前创建了。


function myFun() {
    var name = "jason"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName:function(){ // 在词法作用中,他的outer是myFun函数
            console.log(test1)
            return name
        },
        setName:function(newName){// 在词法作用中,他的outer是myFun函数
            name = newName
        }
    }
    return innerBar
}
var myFunObj = myFun()  
myFunObj.setName("jason2")  
myFunObj.getName()  
console.log(myFunObj.getName()) 

image.png

执行流程:

  1. 调用myFun方法创建执行一个空的上下文
  2. 快速扫描当前代码,如果发现有内部定义的方法,访问外部的变量时,会创建一个对象,叫closure(myFun),它会指向堆中的。如发现getName访问了 test1和 name,setName访问了 name, 会把这两个变量 test1和 name加入到堆中。(closure(myFun)一个内部对象,JavaScript 是无法访问的)
  3. 其他正常的变量如test2 还是正常存在 变量环境或词法环境中。
  4. 当函数myFun执行完时,执行上下文会完全释放,但是由于里面还有 getName 和 setName 引用着 closure(myFun)对象。只能等getName和 setName都完全释放才能正常释放。又因为myFunObj指向着getName,setName。 所以只有在myFunObj指向null,getName 和 setName才会是否,对应的闭包堆,才会被垃圾回收器回收。

深度拷贝代码

function copy(dest){
  if(typeof dest === 'object'){
    if(!dest) return dest; // null
    var obj = dest.constructor();  // Object/Array
    for(var key in dest){
      obj[key] = copy(dest[key])
    }
    return obj;
  } else {
    return dest;
  }
}

2.垃圾回收

  • C语言
    • C语言中 内存是自己创建,自己释放,当内存没正常释放,就会产生内存泄露
// 在堆中分配内存
char* p =  (char*)malloc(2048);  // 在堆空间中分配 2048 字节的空间,并将分配后的引用地址保存到 p 中
 
 // 使用 p 指向的内存
 {
   //....
 }
 
// 使用结束后,销毁这段内存
free(p);
p = NULL;
  • javascript/java/python
    • 通过垃圾回收器统一回收

栈/堆回收

  • 调用栈回收
    • 记录当前执行状态的指针 ESP
    • 通过 ESP 下移动作来实现执行上下文的释放
  • 堆中数据回收
    • 垃圾回收器,检查没有被引用的对象

当EPS下移,当前的myFun执行上下文都会销毁 image.png

当把obj 设置为 null,则垃圾回收会定期回收掉堆中数据 image.png

新生代/老生代

  • 代际假说The Generational Hypothesis

    • 大部分对象都会在内存里面存活的时间很短
    • 不死的对象会存活更久
  • 新生代

    • 生存时间短
    • 1-8m容量
    • 副垃圾回收
  • 老生代

    • 生存时间长
    • 可以很大
    • 主垃圾回收

垃圾回收流程

  1. 标记: 标记空间中的活动与非活动,非活动可以被回收
  2. 清除: 把非活动对象清理
  3. 整理: 由于清空的内容块不是连续的,所以需要重新整理为连续,以便提供大的连续内存

副/主垃圾回收器

副垃圾回收

主要处理小空间内存

image.png

  • scavenge算法
    • 对象区域
    • 空闲区域
    • 流程
      1. 当对象区域快满的时候,先标记垃圾数据
      2. 把活动对象复制到空闲区域,同时顺便进行了排序,所以粘贴后是有序的,复制完把当前活动区直接清空
      3. 把空闲区域和对象区域 做互换,互换后就把刚刚整理好了活动对象放在了对象区域
  • 对象晋升策略
    • 如果连续两次垃圾回收依然存活,对象就被移到老生区中

主垃圾回收

主要处理大空间内存

  • 特点
    • 对象空间大
    • 存活时间长

1. [标记-清除] mark-sweep算法

  1. 标记: 从一个根节点开始递归遍历,能到达的对象就是活动对象,没到达就是垃圾数据
  2. 清除: 把标记为垃圾的全部遍历清理 缺点:容易产生不连续的碎片

清除掉红色标记数据的过程 image.png

2. [标记-整理] mark compact算法

  1. 标记: 从一个根节点开始递归遍历,能到达的对象就是活动对象,没到达就是垃圾数据
  2. 整理:让活动对象移动到另外一端,然后直接清空掉另外一段以外的所有内存

标记过程仍然与 [标记-清除] 算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存 image.png

全停顿stop the world

image.png

  • 新生代由于操作的内容不大,所以标记,清理,整理也不需要很多时间,不影响js主线程执行
  • 老生代由于内容很大,所以标记,清理,整理十分耗时,进行垃圾回收大内存时候,会出现严重的卡顿
  • 标记,清理,整理三个步骤中,标记是最消耗时间,通过增量标记优化,把一个大的任务拆分成多个小任务,执行不阻塞主线程,有点像react 中的fiber,利用浏览器空闲时间做大运算。

image.png

3.编译器和解释器

编译型语言

  • C/C++
  • 执行前,先编译成二进制文件,机器能够直接识别和执行
  • 源代码 -> AST -> 中间代码 -> 优化后的二进制文件/机器码 -> 直接执行
  • 编译器 TurboFun 涡轮增压

image.png

解释型语言

  • javascript(早期),python
  • 每次运行都要通过解释器动态解释和执行
  • 源代码 -> AST -> 解释器转为 字节码 -> 解释器执行
  • 解释器Ignition 打火器

image.png

js执行代码的流程

现代js引擎主要使用的是 解析器 和 编译器 混用JIT。默认还是走解析器的逻辑,但遇到重复热点代码代码则走编译器逻辑。

image.png

JIT技术Just in Time 解释和编译混用

  1. 解释器逐行执行,当判断有重复执行多次的代码 设置为HotSpot,热点代码
  2. 编译器 把热点代码编译为 机器码
  3. 然后再次执行这段代码会直接跳过字节码,执行机器代码

编译

JavaScript 代码在执行前的预处理过程

  1. 源代码
  2. 词法分析
  3. 语法分析
  4. 生成AST
  5. 作用域分析,遍历树包括词法作用域标记,作用域分析器需要确定标识符在什么地方声明,以及代码的哪些位置可以访问它们
  6. 代码生成,解释器生成字节码

执行

解释器执行字节码或二进制码,并逐行执行。

  1. 创建执行上下文
  2. 变量与函数的初始化,主要是变量提升时候,初始赋值undefined,或者函数声明提前定义
  3. 变量赋值和表达式的求值(变量根据作用域的规则标记访问)
  4. 控制流程

AST抽象语法树

  • 词法分析
    • token
  • 语法分析
    • parse解析
      • 解析成ast
  • Babel 代码转码器
    • 现将源码转AST,es6的ast 转成es5的 ast
    • 利用es5的ast生存js代码
  • eslint
    • 先将源代码转ast
    • 利用ast检查代码规范问题

生成字节码

解释器Ignition

  • 根据ast生成字节码

执行代码

使用解释器把字节码 转为机器代码,并执行。

js性能优化

  1. 提升单次脚本的执行速度,防止js阻塞主线程
  2. 避免大内联脚本,减少解析和编译阻塞主线程
  3. 减少js体积,有效降低内存使用和下载速度

参考

time.geekbang.org/column/intr…