JavaScript 内存空间

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

内存

先从计算机角度说一下内存。内存包括三个部分:

  1. 只读存储器(ROM)
  2. 随机存储器(RAM)
  3. 高速缓冲存储器(Cache)

而其中,高速缓冲存储器(Cache)又分为三种:

  1. 一级缓存(L1 Cache)
  2. 二级缓存(L2 Cache)
  3. 三级缓存(L3 Cache)

当 CPU 需要数据的时候,就会先找缓存,因为缓存最快。缓存找不到,才去找慢一点的内存,然后找到后继续将数据放入缓存,下次还要的时候,还是先找缓存。

简单来说,缓存中的数据只是内存的一部分,但是读写速度最快,所以CPU先找它。

内存生命周期

在 JavaScript 中内存的生命周期可以分为三个阶段:

  1. 分配你所需要的内存
  2. 使用(读/写)分配到的内存
  3. 不需要时将内存释放、归还

为了便于理解,我们使用一个简单的例子来解释这个周期。

1
2
3
let a = 20; // 第一阶段:分配变量 a 所需要的空间
console.log(a + 100); // 第二阶段:使用分配到的内存空间
a = null; // 第三阶段:使用完毕之后,释放内存空间

前两步我们都很好理解,JavaScript 在定义变量的时候就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。

内存泄漏

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

不再用到的内存,没有及时释放,就叫做“内存泄漏”(memory leak)。

垃圾回收机制

有些代码语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。这样的话,对于程序员来说,其实是非常麻烦的。所以大多数代码语言(比如 JavaScript 和 C#,Java 等)都提供自动内存管理,以此来减轻程序员的负担。

这种自动内存管理的操作,我们称之为“垃圾回收机制”(garbage collector)。

原理:垃圾收集器会定期(周期性)找出那些不再继续使用的数据,然后释放其所占内存。

最常见的回收方式分为两种:标记清除和引入计数。

内存与数据

堆内存(heap)和栈内存(stack)是程序运行时内存中分配出来的数据区,用于保存程序运行过程中用到的所有数据。

基础数据类型与栈

栈是放在一级缓存中的,数据被调用的时候放入栈存储空间中,调用完成时,就会被立刻释放。

JavaScript 有六种基础数据类型:undefinednullbooleannumberstringsymbol(ES6)。

JavaScript 中的基础数据类型,都是一些简单的数据段,这些值都有固定的大小,往往保存在栈内存中,由系统自动分配存储空间。

引用数据类型与堆

堆是放在二级缓存中的,它的生命周期由虚拟机的垃圾回收算法来决定。所以调用这些数据的速度要相对来得低一些。

JavaScript 中的引用数据类型,比如数组ArrayObject,他们值的大小是不固定的,所以往往保存在堆内存中。

内存数据存取

我们可以直接操作保存在栈内存中的值,却不能直接操作保存在堆内存中的值。因此针对不同的数据和不同的内存空间,存取数据的方式也不一样。

栈的数据存取

栈(stack)是系统自动分配的内存空间。它对数据的存取原则为“先进后出,后进先出”。

我们可以通过类比乒乓球盒子来分析。如下图左侧:

这种乒乓球的存放方式与栈中存取数据的方式如出一辙。

处于盒子中最顶层的乒乓球E,它一定是最后被放进去的,但可以最先被使用。而我们如果想要使用底层的乒乓球A,就必须将上面的4个乒乓球都取出来,让乒乓球A处于盒子开口处,这样才能拿到乒乓球A。

栈内存中的数据也一样,处于栈底部的数据1一定是最先保存的,处于栈顶部的数据true一定是最后保存的。我们只能操作处于栈顶部的数据,当垃圾回收机制要开始销毁栈内存中无用的数据时,也只能从栈顶部开始销毁。

堆的数据存取

堆(heap)是在程序运行时动态分配的内存空间。它存取数据的方式,则与书架和书非常相似。

对于存放在书架上的书来说,我们并不需要记住每一本放进书架的先后顺序。我们只要知道书的名字,就可以很方便的取出我们想要的书。

而堆存取数据,也就是这个特点。由于 JavaScript 不允许直接操作堆内存空间里保存的数据,所以引用类型的数据除了将实际的数据值保存在堆空间中以外,还会在栈空间中保存了当前数据的引用(这里的引用,我们可以粗浅地理解为数据在堆空间中的地址)。

那么在操作引用类型的数据时,实际上是先通过栈空间中的引用去找到堆空间中的数据然后才进行操作。也就是说,我们只需要知道堆中每一条数据的引用(地址),就可以通过引用(地址)找到对应的数据,而不用去关心数据存进堆中的先后顺序。

图解举例

为了更好的搞懂栈内存与堆内存,我们可以结合以下例子与图解进行理解。

1
2
3
4
5
6
var a = 0; // 栈
var b = "hello world"; // 栈
var c = null; // 栈
var obj = { m: 20 }; // 变量 obj 存在于栈中,{ m: 20 } 作为对象存在于堆内存中
var arr = [1, 2, 3]; // 变量 arr 存在于栈中,[1, 2, 3] 作为对象存在于堆内存中

图解:

因此,当我们要访问堆内存中的引用数据类型(如对象,数组,函数等)时,实际上我们首先是从栈中获取该对象的地址引用(或者地址指针),然后再通过这个地址从堆内存中取得我们需要的数据。

总结

我们通过一个表格对栈内存和堆内存做一个总结:

栈内存 堆内存
保存基本数据类型 保存引用数据类型
数据通过值访问 数据通过地址访问
保存的值大小固定 保存的值大小不定,可动态调整
由系统自动分配内存 由程序员通过代码分配内存
主要用来执行程序 主要用来存放对象
空间小,运行效率高 空间大,运行效率较低
存取顺序为先进后出 无序存储,根据引用(地址)直接获取
我 秦始皇 打钱