程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
但是对于前端开发来说,内存并不是一个经常被提及的概念,很容易被大家忽视。可如果想要对 JavaScript 的理解更加深刻,就必须对内存有一个清晰的认知。
内存生命周期
在 JavaScript 中内存的生命周期分为三个阶段:
- 分配你所需要的内存
- 使用(读/写)分配到的内存
- 不需要时将内存释放、归还
为了便于理解,我们使用一个简单的例子来解释这个周期。
|
|
前两步我们都很好理解,JavaScript 在定义变量的时候就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。
内存泄漏
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
不再用到的内存,没有及时释放,就叫做“内存泄漏”(memory leak)。
垃圾回收机制
有些代码语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。这样的话,对于程序员来说,其实是非常麻烦的。所以大多数代码语言(比如 JavaScript 和 C#,Java 等)都提供自动内存管理,以此来减轻程序员的负担。
这种自动内存管理的操作,我们称之为“垃圾回收机制”(garbage collector)。
原理:垃圾收集器会定期(周期性)找出那些不再继续使用的数据,然后释放其所占内存。
最常见的回收方式分为两种:标记清除和引入计数。
堆和栈
堆(heap)和栈(stack)是程序运行时内存中分配出来的数据区,用于保存程序运行过程中用到的所有数据。
基础数据类型与栈
JavaScript 有六种基础数据类型:undefined
、null
、boolean
、number
、string
、symbol
(ES6)。
JavaScript 中的基础数据类型,都是一些简单的数据段,这些值都有固定的大小,往往保存在栈内存中,由系统自动分配存储空间,我们可以直接操作保存在栈内存空间的值.
结论:基础数据类型都是通过数据值进行访问。
引用数据类型与堆
JavaScript 中的引用数据类型,比如数组Array
,他们值的大小是不固定的。所以引用数据类型的值保存在堆内存中。
但是 JavaScript 不允许直接操作堆内存空间里保存的数据。所以引用类型的数据除了将实际的值保存在堆空间中以外,还需要在栈空间中保存了当前数据的引用(这里的引用,我们可以粗浅地理解为数据在堆空间中的地址)。那么操作引用类型的数据时,实际上是先通过栈空间中的引用去找到堆空间中的数据然后才进行操作。
结论:引用类型的值都是通过数据引用(地址)进行访问。
数据存取方式
数据在堆内存和栈内存中的存取方式是不一样的。
栈的数据存取
栈(stack)是系统自动分配的内存空间。它对数据的存取原则为“先进后出,后进先出”。
我们可以通过类比乒乓球盒子来分析。如下图左侧:
这种乒乓球的存放方式与栈中存取数据的方式如出一辙。
处于盒子中最顶层的乒乓球E,它一定是最后被放进去的,但可以最先被使用。而我们如果想要使用底层的乒乓球A,就必须将上面的4个乒乓球都取出来,让乒乓球A处于盒子开口处,这样才能拿到乒乓球A。
栈内存空间中的数据也一样,处于栈底部的数据1
一定是最先保存的,处于栈顶部的数据true
一定是最后保存的。我们只能操作处于栈顶部的数据,当垃圾回收机制要开始销毁栈内存中无用的数据时,也只能从栈顶部开始销毁。
堆的数据存取
堆(heap)是在程序运行时动态分配的内存空间。堆存取数据的方式,则与书架和书非常相似。
对于存放在书架上的书来说,我们并不需要记住每一本放进书架的先后顺序。我们只要知道书的名字,就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓球一样,非得将上面的所有乒乓球都拿出来才能取到下面的某一个乒乓球。
而堆存取数据,也就是这个特点,我们只需要知道堆中每一条数据的引用(地址),就可以通过引用(地址)找到对应的数据,而不用去关心数据存进堆中的先后顺序。
图解举例
为了更好的搞懂栈内存与堆内存,我们可以结合以下例子与图解进行理解。
|
|
图解:
因此,当我们要访问堆内存中的引用数据类型(如对象,数组,函数等)时,实际上我们首先是从栈中获取该对象的地址引用(或者地址指针),然后再通过这个地址从堆内存中取得我们需要的数据。
引用类型的特点
大致了解了 JavaScript 的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的特点了。
例一:
|
|
图解:
在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a
执行之后,a
和 b
的值虽然都等于 20,但是他们其实已经是相互独立互不影响的值了。所以,我们修改了 b
的值以后,a
的值并不会发生变化。
例二:
|
|
图解:
当我们通过 var person2 = person1
执行一次复制引用类型的操作时,引用类型的复制也会为新的变量分配一个新的值保存在栈内存中。但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是这两个地址指针指向的都是堆内存中的同一个地方,所以在堆内存中访问到的具体对象实际上是同一个。
因此,当我们改变 person2
的时候,就是在改变堆内存中 person2
指向的这个对象。而同时,person1
指向的也是这个对象,所以 person1
也会跟着发生变化。这就是引用类型的特性。
总结
我们通过一个表格对栈内存和堆内存做一个总结:
栈内存 | 堆内存 |
---|---|
保存基本数据类型 | 保存引用数据类型 |
通过值访问 | 通过地址访问 |
保存的值大小固定 | 保存的值大小不定,可动态调整 |
由系统自动分配内存 | 由程序员通过代码分配内存 |
主要用来执行程序 | 主要用来存放对象 |
空间小,运行效率高 | 空间大,运行效率较低 |
存取顺序为先进后出 | 无序存储,根据引用(地址)直接获取 |