主要包含三部分:堆空间、栈空间、代码空间。javascript
先看一段代码:java
function foo() {
var a = "极客时间";
var b = a;
var c = { name: "极客时间" };
var d = c;
}
foo();
复制代码
普通类型
的变量都被保存在执行上下文中
,执行上下文又被压入到栈中。而引用类型
的变量则是在栈中存放访问堆空间的地址,其值被保存在堆空间中
。JS 中的垃圾数据分为:栈中的垃圾数据与堆中的垃圾数据算法
如下面这段代码为例分析:浏览器
function foo() {
var a = 1;
var b = { name: "极客邦" };
function showName() {
var c = "极客时间";
var d = { name: "极客时间" };
}
showName();
}
foo();
复制代码
经过 ESP 指针(即记录当前执行状态的指针)来操做,ESP 指向 showName 的函数上下文时,表示当前正在执行 showName 函数。性能优化
当 showName 执行完毕后,ESP 下移指向 foo 的执行上下文,这个操做就是销毁 showName 的函数执行上下文。网络
下图为从栈中回收 showName 执行上下文的过程
架构
当 foo 执行结束后,ESP 就指向全局上下文了,此时内存状态以下图。
函数
在 V8 中把堆分为新生代
和老生代
两个区域,新生代存放生存时间很短的对象,容量只有 1~8M;老生代中存放生存时间久的对象,容量很大。性能
对应的就有两种垃圾回收器,副垃圾回收器与主垃圾回收器。 主垃圾回收器:主要负责老生代的垃圾回收。 副垃圾回收器:主要负责新生代的垃圾回收。大数据
两种回收器有一套共同的执行流程。
主要用于回收新生代的垃圾,故内存不大。
下图为 V8 中的堆空间分布。
新生代经过 Scavenge 算法将空间划分为对象区域与空闲区域。 每当对象区域被写满时,就会执行一次垃圾回收,具体过程以下:
因为复制操做须要时间成本且操做频繁,因此为了执行效率,新生代的空间都不会太大;若通过两次 GC 回收依然存活,就会将活着的对象移到老生代中,这就是 JS 引擎的对象晋升策略
。
主要用于回收老生代的垃圾,除了新生代中晋升的对象,一些大的对象会被直接分配到老生代。
老生代对象的两个特色:
基于上述两个特色,主垃圾回收器采用标记-清除(Mark-Sweep)
的算法进行垃圾回收。
标记:从一组根元素开始,递归遍历这组根元素,能到达的元素成为活动对象
,没有到达的为垃圾数据
。
function foo() {
var a = 1;
var b = { name: "极客邦" };
function showName() {
var c = "极客时间";
var d = { name: "极客时间" };
}
showName();
}
foo();
复制代码
仍是这段代码,当 showName 函数退出后,调用栈和堆空间以下图:
下图为垃圾清除的过程
对一块内存屡次执行标记-清除算法后,会产生大量不连续的内存碎片。因而又出现了另外一种算法标记-整理(Mark-Compact)
,标记过程都是同样的,标记完后将全部的活动对象都向一端移动,而后直接清除掉端边界之外的内存。
全停顿:JS 是运行在主线程之上的,一旦执行垃圾回收,就须要将正在执行的 JS 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行,这期间应用的性能和响应能力都会直线降低。这个过程就叫作全停顿(Stop-The-World)。
V8 新生代垃圾回收由于空间小,存活对象少,全停顿影响不大;但老生代就不同了,好比正在执行 JS 动画,GC 工做致使主线程不能作其余事情,那个动画在这段时间没法执行,页面就会卡顿。
为下降卡顿,因而诞生了增量标记(Incremental Marking)算法
。 增量标记:V8 将标记过程分为一个个子标记过程,同时让垃圾回收器和 JS 应用逻辑交替进行,直至标记完成。这些小任务执行时间短,穿插再 JS 任务间执行,这样就不会感觉到页面卡顿了。
编译型语言
在程序执行前,需通过编译器编译,而且编译以后会直接保留机器能读懂的二进制文件;之后每次运行程序时,直接运行二进制文件,不须要从新编译。
解释型语言 在每次运行时都要通过解释器对程序进行动态解释和执行。
将源代码转换为编译器和解释器能够理解的 AST,并生成执行上下文。
为何要生成 AST 以及什么是 AST?
先说第一个,由于编译器和解释器没法理解高级语言,因此要生成 AST,就像渲染引擎将 HTML 转换为计算机能够理解的 DOM 树同样。
再来讲第二个,AST 是代码的结构化表示,有着很是重要的做用。
Babel 原理: 将 ES6 源码转换为 AST,再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用它来生成 ES5 源代码。
ESLint 原理:检测流程也是将源码转换为 AST,再利用 AST 来检查代码规范。
如何生成 AST?
两个阶段:分词(词法分析),解析(语法分析)。
词法分析:将源码拆解成最小的、不可再分的 token(关键字、标识符、赋值、字符串)。
语法分析:根据规则将上一步的 token 转换为 AST。若源码存在语法错误,则不会生成 AST。
有了 AST 之后,V8 就会生成这段代码的执行上下文。
有了 AST 和执行上下文后,解释器根据 AST 生成字节码并解释执行。
为何不直接转成机器码,而是经过字节码转成机器码? 看下图:
由上图能够看出,机器码占用的空间远大于字节码,早期手机内存只有 512M,而 V8 须要消耗大量内存来存放转换后的机器码,为了解决内存占用问题,就有了如今这套架构。
字节码:介于 AST 与机器码之间的一种代码,须要经过解释器将其转为机器码后才能执行。
第一次执行字节码时,解释器会逐条解释执行,若是发现有热点代码
(即一段代码被重复执行屡次),编译器就会将这段热点代码编译为机器码保存起来,当再次执行这段代码时,只须要执行编译后的机器码;这种技术就叫作即时编译(JIT)
。
将优化的中心聚焦在单次脚本执行时间和脚本的网络下载上。