一、内存
Javascript
程序的运行,不管是依赖于浏览器还是 Node.js
环境,最终的运行都依赖于操作系统,而操作系统的运行依赖于计算机硬件资源。使用 Javascript
语言开发的程序最终会变成一条条指令和数据,要依赖于计算机硬件的执行和存储。
比如两个变量执行加法运算, 就需要 CPU
运算单元的加法器提供支持,而参与运算数据的存储就要占用内存空间。
当然,计算机的硬件资源(CPU
的运算资源和缓存空间、内存条的内存空间等等)是有限的,不可能无限制的使用。所以,程序运行的时候产生的一些没用的中间数据,要及时的清理掉,释放出空间,留出的空间用以存储其他数据,这就是所谓的垃圾回收。
举个简单的例子:
我们要计算 1+2+4
的结果,那么 1+2
的和 3
就是一个临时数据,只用到它来加 4
得到最终结果 7
存在内存中,之后 3
就不会再占用具体的内存空间了。
二、内存的生命周期
不管什么程序语言,内存生命周期基本是一致的:
-
分配你所需要的内存
-
使用分配到的内存(读、写)
-
不需要时将其释放 / 归还
像 C
语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()
和 free()
。相反,JavaScript
是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时自动释放。 释放的过程称为垃圾回收。这也让 JavaScript
开发者错误的感觉可以不关心内存管理。
二、内存分配
程序执行的时候内存空间会分为代码区和数据区,数据区又可以分为静态存储区和动态存储区,动态存储区又可以分为堆区和栈区。 如下图:
以下面的代码为例,分别对几个名字做一下简单介绍:
//常量 PI,分配在静态存储区
const PI = 3.1415926;
//全局变量 S,分配在静态存储区
let S = 0;
//局部变量 i,分配在栈区
for (let i = 0; i < 10; i++) {
S += i;
}
function fun() {
// 字符串数据分配在栈区
let str = "";
// 数组数据分配在堆区,数据的地址变量 arr 分配在栈区
let arr = [];
// 对象数据分配在堆区,数据的地址变量 obj 分配在栈区
let obj = {};
}
-
代码区
Javascript
代码要想被CPU
执行,都要被编译处理成CPU
可以读取运行的指令(CPU
都有自己硬件层面的指令集)。- 代码区存放的是
for
、if
、while
等程序结构经过解析编译处理后存放在内存中的程序指令,这些程序指令可以控制CPU
的运行,CPU
中有相关的寄存器可以处理这些程序指令(具体的内部硬件原理我们可以不用管, 只需要知道代码区存储的二进制程序指令可以控制CPU
的状态)。比如代码中定义的for
循环结构,它的功能就是多次循环执行里面包裹的的加法运算。在初始化Javascript
代码的时候,for
循环程序会被处理为一条一条的程序指令存放在内存的代码区,当CPU
的程序计数器指向内存中代码区程序指令的时候,就会循环调用自己的硬件加法器完成相关的运算。
-
数据区
- 数据区就是存储数据的。具体来说就是:声明的变量、字符串、数据等数据,
CPU
在运算的时候会从内存中取出对应的数据,同时,计算出的数据也会存入内存。
- 数据区就是存储数据的。具体来说就是:声明的变量、字符串、数据等数据,
-
静态存储区
- 处理
Javascript
代码的时候,浏览器会在内存静态存储区开辟空间存储常量PI
, 使用const
声明PI
主要是为了编写程序方便,圆周率3.1415926
是定值,每次调用麻烦。编写程序的时候使用const
声明一次,PI
这个符号就可以代表圆周率,凡是程序中出现PI
的地方,就会默认它是3.1415926
。 const
就是一个标识符号。当浏览器或者nodejs
阅读到它的时候,就知道要const
声明的数据存储到内存静态存储区,只要程序处于运行状态它都存在于内存中,也就是说常量生命周期贯穿整个程序运行期间,除非程序退出。比如关闭浏览器时,内存中静态存储区的数据就会清空释放,但是全局变量S
和常量PI
一样也会存储在内存静态存储区。
- 处理
-
栈
-
当
CPU
指向for
关键字定义的程序被处理后存储在内存代码区的程序指令,就会在内存数据区开辟空间存储局部变量i
,具体点就是数据区动态存储区的栈区。 -
当
for
循环程序执行结束的时候,变量i
就会被销毁,释放栈区空间。 -
从这里可以看出栈区数据的生命周期很短,在这个过程中,静态存储区存放的全局变量
S
的值经过多次加法赋值运算已经发生了变化,只要整体Javascript
程序不退出它就会一直存在。讲到这里, 你应该会更加深刻理解局部变量和全局变量了,全局变量的生存状态依赖于整个Javascript
程序,局部变量的存在依赖于局部一个程序段,比如一个函数中的局部变量,if
语句中的局部变量,for
循环结构程序中的局部变量。 -
示意图:
var name = '陈星星'; var age = 18; var city = 'Wuhan';
-
-
堆
-
上面的代码中有一个函数,函数里初始化了一个数组,这个时候,
CPU
就会在内存数据区开辟内存空间存储一个新的数组,具体来说就是数组被存储到了动态内存的堆区。 -
但是数组的地址
arr
被存储到栈区,CPU
通过栈区数组的地址 可以访问堆区的数据数据,比如arr[0]
指针指向数组的第一个元素。这里你可以看出访问堆区的数据要经过一个中间区栈区,说明栈区数据的执行效率高。 -
计算机的硬件资源是有限的,
CPU
的运算能力是有限的,所以要优化算法;内存的存储空间是有限的, 因此要对内存进行合理分配。比如局部变量分配在栈区可以快速调用,调用结束然后迅速清除释放。 -
堆区的数据清除和释放与栈区有所不同,栈区数据释放比较简单,只要局部变量的相关程序执行完毕,它就释放。也就是说被释放的数据属于垃圾数据(没有用的数据),那么堆区的数据如何判断是垃圾数据?**堆区的数据的访问是通过栈区的地址,如果栈区的地址不再指向堆区中的数据,那么
Javascript
的解释器就会把该数据占用的内存区域清空释放,Javascript
语言运行的浏览器 或nodejs
平台都会按照一定的规则自动检测堆区数据,一旦发现垃圾数据就会释放,一般不需要程序员手动释放内存(如果是C++
和C
语言需要程序员手动释放)。**不过有些时候还是需要程序员手动干预,对于简单的工程,浏览器队都会自动管理内存,不过复杂的工程还是需要程序员对内存分配、管理有深入的认识。let arr = []; // arr等于空指针,释放内存 arr = null;
null
是一个空值,空指针,一般用于释放堆区数据。arr
原本指向堆区的数组数据,但是你重新给arr
赋值为空指针,不再指向arr
数组,那么该数组占据的内存空间就会被释放。 -
示意图:
var person1 = { name: '陈星星' }; var person2 = { name: 'Deepspace' }; var person3 = { name: '陈鑫' };
-
看下面的示例代码,比较基本类型数据与引用类型的数据有什么区别。
引用类型的数据在堆区中释放的算法,Javascript
程序会对数组等引用数据的引用数量计数,一旦没有引用,就会被清除释放。
function fun() {
// 声明一个变量a,赋值10
let a = 10;
// 重新开辟内存空间存放b并把a的值10赋值给b
const b = a;
// 变量a原来的值10被覆盖重新更改为20,b的值不受影响
a = 20;
console.log('a:' + a, 'b:' + b);
// 声明数组存放在堆区,数组地址存放在栈区
let arr1 = [];
// 声明一个变量 arr2 存储在栈区,作为一个指针,和 arr1 一样指向同一个数组
let arr2 = arr1;
// arr2.push(2);和 arr1.push(2);作用相同都会改变数组结构
// arr1 和 arr2 指的都是同一个数组
arr1.push(2);
console.log(arr1, arr2);
const obj = {};
// 变量 arr2 指向新的数据obj对象
// 注意这不会改变原来的数组,此时 arr2 指向的数组只是少了一个引用
arr2 = obj;
console.log(arr1, arr2);
// 注意此时 arr1 和 arr2 指向的数组都指向了别的对区数据,数组的引用为 0,被清除释放
arr1 = obj;
console.log(arr1, arr2);
}
fun();