高级语言程序最终被编译器(解释器)转换成一条条机器指令,程序最终的执行形式是进程
,进程
为程序提供运行时刻环境(run-time environment),此环境处理许多事务如:为源程序中的对象分配和安排存储位置,过程连接,参数传递,与操作系统、输入输出设备等的接口。
程序在执行前,操作系统需要为该进程分配内存空间,典型的运行时刻内存划分如下:
操作系统分配内存的方式有多种,如连续分配、分页存储、分段存储等。以分页存储为例,内存中存放每一个进程的页表来记录逻辑页号与实际物理磁盘块对应关系,而在页表寄存器中记录每个进程页表始址与页号和页表长度,平时,进程未 执行时,页表的始址和页表长度存放在本进程的 PCB 中。当调度程序调度到某进程时,才 将这两个数据装入页表寄存器中。CPU给出逻辑地址通过分页地址变换找到真实物理块。如图:
对于大内存应用,可以通过虚拟存储器
解决,即加入缺页中断,换入换出机制的内存管理策略:
这其中还涉及到不同置换算法,详细知识请参考《操作系统》,我们在后面的离线应用章节本地存储方面也会涉及到相关知识。
与其他编程语言类似,JS的内存空间同样可分为栈空间和堆空间,JS中那些具有固定大小的基本数据类型(String、Undefined、Null、Boolean、Number、Symbol)存储在栈空间中,而对象都分布在堆内存空间中,在栈空间中存储的是存储于堆空间的对象的引用地址:
这一部分主要针对栈空间详解,堆空间部分见GC机制章节。
每次当控制器转到可执行代码的时候,就会进入一个执行上下文,JavaScript中的运行环境三种情况:
- 全局环境:JavaScript代码运行起来会首先进入该环境
- 函数环境:当函数被调用执行时,会进入当前函数中执行代码
- eval(不建议使用,可忽略)
代码的一次执行通常会有许多个执行上下文,每次过程(函数)调用都会产生一个新的执行上下文,js中通过调用栈(Call stack)来管理这些执行上下文,栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。
然后我们看看每个执行上下文通常有的元素(来自《编译原理》运行时环境章节):
执行上下文的周期:
- 创建阶段 在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向
- 代码执行阶段 创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码
以下面代码为例:
var fn = null;
function foo(d) {
var a = 2;
function innnerFoo() {
console.log(a);
}
innnerFoo();
fn = innnerFoo;
}
function bar() {
fn();
}
foo(6);
bar(); // 2
上面代码实际运行顺序:
function innnerFoo() {
console.log(a);
}
var a;
a=2;
innnerFoo();
fn = innnerFoo;
Chrome调试:
可以看到,当刚刚进入foo()
函数时,就已经确定了this指向、参数值、和作用域链(Scope),并且还创建了变量对象。
变量对象创建的过程:
-
建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
-
检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
-
检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
-
求出
this
值
还是上面的例子,刚进入foo()
函数时,变量对象上就确定了参数的值,并得到了a
的声明并初始化为undefined
,而innerFoo
函数声明也被识别,this
指向window
。
针对第三条,看下面的例子:
console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
最终打印的是函数foo的声明,即后面对foo的声明不会影响foo的值(即指向函数foo()
,执行阶段foo的值由于执行foo=20
而改变)。
执行上下文的创建阶段做了一系列工作后,变量对象中的属性还不能访问,上面例子中的a
也将在执行过程中被赋值:
// 执行阶段
VO -> AO
VO = {
arguments: {...},
a: 2,
innerFoo: <innerFoo reference>,
this: Window
}
js是一门函数式编程语言,支持过程(函数嵌套)定义,这样对于非局部数据的访问就比较麻烦,需要在每个执行上下文中加入访问链(作用域链),作用域链是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。对应的上例chrome断点调试右侧的scope
信息。
正是由于这种作用域链的特性,出现一个重要的概念:闭包
还是上面的例子,这个时候我们执行到innerFoo()
函数内部:
可以看到这个时候Scope
链多了一个Closure(foo)
闭包是一种特殊的对象。它由两部分组成:执行上下文(foo()
),以及在该执行上下文中创建的函数(innerFoo()
)。
当innerFoo()
执行时,如果访问了foo()
中变量对象中的值,那么闭包就会产生。Chrome用foo
来表示闭包。
内存空间生命周期:
- 分配所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放、归还
对于函数调用创建的执行上下文,通常当其执行完毕后,其执行环境就被销毁,变量对象所占用的空间都会被回收,但是当我们使用了闭包时,就不一定了:
function assignHandler(){
let element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
}
}
assignHandler();
上面例子中,element
的点击事件处理程序内访问了其包含执行上下文(assignHandler()
)变量对象中的element
变量,这会导致虽然assignHandler()
执行完毕,但是其变量对象还是不能得到回收,一种减少此情况下内存泄漏影响的方法是:
function assignHandler(){
let element = document.getElementById("someElement");
let id = element.id;
element.onclick = function(){
alert(id);
}
element = null;
}
assignHandler();
虽然assignHandler()
的变量对象不能被回收,但是element
变量被显式赋值为null
后,其引用的堆中DOM对象将被回收,降低了内存泄漏影响。
全局上下文将一直存在直到程序结束。对于全局上下文的变量对象所占用的空间,尤其是在堆中分配的对象空间的管理,我们将在垃圾回收机制章节中详解。