JS进阶系列-第二篇-内存空间

Posted by Kylen on 2019-07-17

前言

像C语言这样的底层语言一般都有底层的内存管理接口,像malloc()free()。像JavaScript这样的语言在创建变量的时候自动完成了内存的分配,并且在不需要的时候自动释放。释放的过程称之为“垃圾回收”。这个“自动”就是混乱的根源,它让我们以为使用JavaScript code就不用关心内存管理。

JavaScript的内存空间

我们coding的时候应该都想过一个问题,我们定义的变量都存储在何处?我们知道程序在运行的时候会创建进程,创建进程会分配资源。这里的资源就包括内存。

一说到内存,就会想到栈内存和堆内存,对于栈内存和堆内存的理解可以看数据结构的堆、栈和操作系统的堆内存、栈内存的区别

局部的、占用空间确定的数据一般都放在stack中,反之就放在heap中。对应的JavaScript的变量,基本类型:string、number、boolean、null(不要在意它的type是object)、undefined、symbol存储在栈区;复杂数据类型(引用类型):object,像array、function在JavaScript中也都是object,一般都是存储在堆区。

在区分了栈区和堆区之后,我们就能更轻易的理解基本类型的数据和引用类型的数据的区别。

举个例子:

1
2
3
4
5
6
7
8
9
10
var a1 = 0;   // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象

var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中
function foo(){
var d = 10
}
foo()

基本数据类型和复杂类型(引用类型)的区别
js-advanced 2019-07-16 下午5.31.59.png
变量对象的在执行上下文栈中的状态
js-advanced 2019-07-22 下午2.49.20.png

变量对象中的基本数据类型的数据都是直接以栈的形式存储,而引用类型的数据本体存储在堆区,栈区只存储地址的引用(或者说指针)。这也就很容易解释JS中一个常见现象。关于执行上下文的内容可以看JS进阶系列-第四篇-执行上下文

1
2
3
4
5
// demo02.js
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
console.log(m.a); // 15

再来一个复杂点的,稍微注意下连等号和两个赋值操作的区别,得到引用在赋值操作之前,得到引用从左到右,赋值操作从右到左

1
2
3
4
5
6
var a = {n : 1};
var b = a;
// a.x = a = {n: 2};
b.x = a = {n: 2}
a.x // undefined
b.x // {n: 2}

js-advanced 2019-07-17 上午11.27.59.png

小结:执行上下文栈以栈的形式组织和管理,每个执行上下文都有自己的变量对象,在变量对象的中基本类型直接存储,引用类型存储在堆中。所以可以说基本类型存储在栈中,引用类型存储在堆中。注意同一个变量对象中的数据是在栈的统一层级,不要对“基本类型数据存储在栈中”这句话产生误解。

内存管理

前面说到JavaScript的内存是自动的,内存的生命周期一般为:
1.分配你所需要的内存,
2.使用你分配的内存(读、写),
3.释放内存
这里的自动主要包括自动分配和自动回收(释放),所以在使用JavaScirpt coding时只会有看到“使用内存”。这也是忽略内存管理的主要原因。

自动分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存

var o = {
a: 1,
b: null
}; // 给对象及其包含的值分配内存

// 给数组及其包含的值分配内存(就像对象一样)
var a = [1, null, "abra"];

function f(a){
return a + 2;
} // 给函数(可调用的对象)分配内存

// 函数表达式也能分配一个对象
someElement.addEventListener('click', function(){
someElement.style.backgroundColor = 'blue';
}, false);

自动回收

JavaScript的自动回收指在内存不再需要的时候释放,如何确定内存不再需要却是一个难题。所以自动回收是一个不断查找内存是否还被需要的机制。自动回收不是全能的,它是一个近似的过程,它不能确定某个内存一定不被需要了,所以只能解决一般问题。

垃圾回收机制隔一段时间检查一下那些不被需要了就将其释放。

这里主要介绍一下垃圾回收的算法,也就是检查变量不被需要的算法。

引用计数垃圾收集
最初级的垃圾回收算法,此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var o = { 
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1; // 现在,“这个对象”的原始引用o被o2替换了

var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 最初的对象现在已经是零引用了
// 他可以被垃圾回收了
// 然而它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了

限制:无法处理循环引用

标记清除法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

对于JavaScript来说,在局部作用域中,函数调用完毕,局部变量就没有必要存在了。

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

参考链接

内存空间详解
内存管理
关于js中 “栈空间的先进后出,后进先出” 的疑问?