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 地址,不存储具体的值。每次访问都动态再去堆里找内容。
如果把堆直接放在栈中,会影响整个栈中上下文的切换。
如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())
执行流程:
- 调用myFun方法创建执行一个空的上下文
- 快速扫描当前代码,如果发现有内部定义的方法,访问外部的变量时,会创建一个对象,叫closure(myFun),它会指向堆中的。如发现getName访问了 test1和 name,setName访问了 name, 会把这两个变量 test1和 name加入到堆中。(closure(myFun)一个内部对象,JavaScript 是无法访问的)
- 其他正常的变量如test2 还是正常存在 变量环境或词法环境中。
- 当函数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执行上下文都会销毁
当把obj 设置为 null,则垃圾回收会定期回收掉堆中数据
新生代/老生代
-
代际假说The Generational Hypothesis
- 大部分对象都会在内存里面存活的时间很短
- 不死的对象会存活更久
-
新生代
- 生存时间短
- 1-8m容量
- 副垃圾回收
-
老生代
- 生存时间长
- 可以很大
- 主垃圾回收
垃圾回收流程
- 标记: 标记空间中的活动与非活动,非活动可以被回收
- 清除: 把非活动对象清理
- 整理: 由于清空的内容块不是连续的,所以需要重新整理为连续,以便提供大的连续内存
副/主垃圾回收器
副垃圾回收
主要处理小空间内存
- scavenge算法
- 对象区域
- 空闲区域
- 流程
- 当对象区域快满的时候,先标记垃圾数据
- 把活动对象复制到空闲区域,同时顺便进行了排序,所以粘贴后是有序的,复制完把当前活动区直接清空
- 把空闲区域和对象区域 做互换,互换后就把刚刚整理好了活动对象放在了对象区域
- 对象晋升策略
- 如果连续两次垃圾回收依然存活,对象就被移到老生区中
主垃圾回收
主要处理大空间内存
- 特点
- 对象空间大
- 存活时间长
1. [标记-清除] mark-sweep算法
- 标记: 从一个根节点开始递归遍历,能到达的对象就是活动对象,没到达就是垃圾数据
- 清除: 把标记为垃圾的全部遍历清理 缺点:容易产生不连续的碎片
清除掉红色标记数据的过程
2. [标记-整理] mark compact算法
- 标记: 从一个根节点开始递归遍历,能到达的对象就是活动对象,没到达就是垃圾数据
- 整理:让活动对象移动到另外一端,然后直接清空掉另外一段以外的所有内存
标记过程仍然与 [标记-清除] 算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
全停顿stop the world
- 新生代由于操作的内容不大,所以标记,清理,整理也不需要很多时间,不影响js主线程执行
- 老生代由于内容很大,所以标记,清理,整理十分耗时,进行垃圾回收大内存时候,会出现严重的卡顿
- 标记,清理,整理三个步骤中,标记是最消耗时间,通过增量标记优化,把一个大的任务拆分成多个小任务,执行不阻塞主线程,有点像react 中的fiber,利用浏览器空闲时间做大运算。
3.编译器和解释器
编译型语言
- C/C++
- 执行前,先编译成二进制文件,机器能够直接识别和执行
- 源代码 -> AST -> 中间代码 -> 优化后的二进制文件/机器码 -> 直接执行
- 编译器 TurboFun 涡轮增压
解释型语言
- javascript(早期),python
- 每次运行都要通过解释器动态解释和执行
- 源代码 -> AST -> 解释器转为 字节码 -> 解释器执行
- 解释器Ignition 打火器
js执行代码的流程
现代js引擎主要使用的是 解析器 和 编译器 混用JIT。默认还是走解析器的逻辑,但遇到重复热点代码代码则走编译器逻辑。
JIT技术Just in Time 解释和编译混用
- 解释器逐行执行,当判断有重复执行多次的代码 设置为HotSpot,热点代码
- 编译器 把热点代码编译为 机器码
- 然后再次执行这段代码会直接跳过字节码,执行机器代码
编译
JavaScript 代码在执行前的预处理过程
- 源代码
- 词法分析
- 语法分析
- 生成AST
- 作用域分析,遍历树包括词法作用域标记,作用域分析器需要确定标识符在什么地方声明,以及代码的哪些位置可以访问它们
- 代码生成,解释器生成字节码
执行
解释器执行字节码或二进制码,并逐行执行。
- 创建执行上下文
- 变量与函数的初始化,主要是变量提升时候,初始赋值undefined,或者函数声明提前定义
- 变量赋值和表达式的求值(变量根据作用域的规则标记访问)
- 控制流程
AST抽象语法树
- 词法分析
- token
- 语法分析
- parse解析
- 解析成ast
- parse解析
- Babel 代码转码器
- 现将源码转AST,es6的ast 转成es5的 ast
- 利用es5的ast生存js代码
- eslint
- 先将源代码转ast
- 利用ast检查代码规范问题
生成字节码
解释器Ignition
- 根据ast生成字节码
执行代码
使用解释器把字节码 转为机器代码,并执行。
js性能优化
- 提升单次脚本的执行速度,防止js阻塞主线程
- 避免大内联脚本,减少解析和编译阻塞主线程
- 减少js体积,有效降低内存使用和下载速度