Javascript内存管理

  如果你从C语言、C++语言的内存手动管理一路走来,我相信你学习几十分钟Javascript的内存管理,就可以很好的管理Javascript的内存问题, 当你来认真学习Javascript内存管理的时候,说明并没有其它语言的内存管理经验,没有经过系统的编程训练,或者说你对其它语言的使用往往都是浅尝辄止, 第一次接触Javascript往往也是因为工作驱动去学习,可能也没有时间重 新去学习操作系统、内存管理,所以下面的解释尽量以具体的Javascript代码作为辅助说明,如果仅仅按照书本上的专业名词概念解释,只会导致越说越糊涂。

内存

  Javascript程序的运行依赖于浏览器,浏览器的运行依赖于操作系统,操作系统的运行依赖于计算机硬件资源。这些知识不是几本书能讲完的,更不是几节课 可以说清楚的。不管中间隔了多少层,使用Javascript脚本语言开发的程序最终会变成一条条指令和数据,要依赖于计算机硬件的执行和存储。比如两个变量执行加法运算, 就需要CPU运算单元的加法器提供支持,而参与运算数据的存储就要占用内存空间,至于什么是寄存器,什么是加法器,不用深究,你只需要知道CPU的硬件运算资源有限,所以要优化 编程的算法来节约有限的运算资源,计算机内存条的内存空间、CPU的缓存空间也都是有限的,那也就是说,程序运行的时候产生的一些没用的中间数据,要及时的清理掉, 比如你的程序只想知道5x8x2的结果,那么5和8的乘积40就是一个临时数据,最后乘以2计算出80存入内存中,40就不会再占用具体的内存空间,腾出的空间可以存储其他数据。

初始化

  大家平时会把Javascript程序写在.html或.js格式文档中,这些文档在本地计算机或服务器中,和普通文件一样放在硬盘中存储。浏览器是一个依赖于操作系统运行的应用软件,与普通客户端软件 不同的是这款软件内置了Javascript解释器,可以解析Javascript语句,调用CPU执行相关指令。当浏览器从本地打开或者从服务器下载运行Javascript程序所在的文件,浏览器的Javascript解释器首先要做的就是 阅读解析代码,给变量数据分配内存,操作系统、CPU、内存等会调整到你的Javascript程序定义的工作状态,这个过程你可以理解为程序的初始化过程, 这里对初始化的解释不够严谨,会有所偏差,不过结合Javascript语言和浏览器解释有助于 大家理解,详细专业解释可以查看百科。浏览器解析代码初始化后就能够对外界的相关用户交互操作做出反应, 比如你在程序中定义了一个鼠标事件,当你使用鼠标进行相关操作的时候就会触发这个事件,相关的程序就会执行,执行的时候,内存中的相关数据就会成为操作数在CPU控制下参与运算处理。 不管初始化还是执行某程序,内存中都会产生新的数据,同时会 清除没用的数据释放内存。在这个过程中不同的数据分配内存、释放内存的方式都有所不同,这也是本节课要研究的内容,内存是如何分配的,数据是如何管理的,什么是垃圾数据需要清除?

文件存储

  平时讲解Javascript语言《数据类型》的时候会说数据的存储方法,整数、浮点数、字符串等数据, 注意这是程序初始化或者程序执行分配内存的时候,Javascript程序没有执行的时候,放在.html或.js文档里面,就是一个文件,就像.txt文本文档一样,如果你有兴趣可以详细研究操作系统的 文件系统和各种格式文档存储问题,文件的存储就像字符串一样,统一按照一定的编码规范存储,比如整数5在.js文件中就是当成字符数据5去存储,只有文档被浏览器或者nodejs解析运行分配内存的时候 才会考虑整型、浮点型不同数据的内存分配问题。你可以新建一个txt文本文档,然后另存为,界面右下角可以看到一个编码方式的下拉菜单,有选项比如ANSI、UTF-8、Unicode等编码标准。对于html和js文件也一样, 具体文件如何存储的不用学习,有个印象就可以,使用WebStorm软件在创建.html、.css、.js或.json文件的时候,会有一个默认的编码格式,查看WebStorm软件界面右下角可以看到默认的编码格式UTF-8,当然你也可以自定义更改。

内存分配

  程序执行的时候内存空间会分为代码区和数据区,数据区又可以分为静态存储区和动态存储区,动态存储区又可以分为堆区和栈区。

内存分配堆、栈静、态存储区
1    //常量PI,分配在静态存储区
2    const PI = 3.1415926;
3    //全局变量S,分配在静态存储区
4    let S = 0;
5    //局部变量i,分配在栈区
6    for(let i = 0;i<10;i++){
7        S += i;
8    }
9    function fun() {
10       // 字符串数据分配在栈区
11       let str = "";
12       // 数组数据分配在堆区,数据的地址变量arr分配在栈区
13       let arr = [];
14       // 对象数据分配在堆区,数据的地址变量obj分配在栈区
15       let obj = {};
16   }

代码区

  CPU都有自己硬件层面支持的指令集,Javascript脚本代码要想被CPU执行,都要被浏览器和操作系统编译处理成CPU可以读取运行的指令。 代码区存放的是for、if、while等程序结构经过浏览器解析编译处理后存放在内存中的程序指令,这些程序指令可以控制CPU的运行,CPU中有相关的寄存器可以处理这些程序指令。具体的内部硬件原理你不用管它, 你只需要知道代码区存储的二进制程序指令可以控制CPU的状态,比如代码第6行到第8行定义的for循环结构,它的功能就是多次循环执行第7行的加法运算。浏览器初始化Javascript代码的时候, for循环程序会被处理为一条一条的程序指令存放在内存的代码区,当CPU的程序计数器指向内存中代码区程序指令的时候就会循环调用自己的硬件加法器完成相关的运算。

数据区

  数据区很容易理解,就是存储数据,具体点说就是比如你声明的变量、字符串、数组等数据,CPU运算的时候会从内存取用数据,计算出的数据也会存入内存。

静态存储区

  浏览器处理Javascript代码的时候,比如解析到第2行代码,浏览器会在内存静态存储区开辟空间存储常量PI,const是Javascript的一个关键字,类似let,只不过let用来声明变量,const用来声明常量,常量简单说就是不变的量, 使用const声明PI主要是为了编写程序方便,圆周率3.1415926是定值,每次调用麻烦,编写程序的时候使用const声明一次,PI这个符号就可以代表圆周率,凡是程序中出现PI的地方,就会默认它是3.1415926。const就是一个标识符号, 浏览器或者nodejs阅读到它,就知道要const声明的数据存储到内存静态存储区,只要程序处于运行状态它都存在于内存中,也就是说常量生命周期贯穿整个程序运行期间,除非程退出, 比如你关闭浏览器内存中静态存储区的数据就会清空释放。第4行定义的全局变量S和常量PI一样也会存储在内存静态存储区。

  当CPU指向第6行for关键字定义的程序被处理后存储在内存代码区的程序指令,就会在内存数据区开辟空间存储局部变量i,具体说是数据区动态存储区的栈区,当for循环程序执行结束的时候,变量i就会被销毁,释放栈区空间,从这里 你也可以看出栈区数据的生命周期很短,在这个过程中,静态存储区存放的全局变量S的值经过多次加法赋值运算已经发生了变化,只要整体Javascript程序不退出它就会一直存在。讲到这里, 你应该会更加深刻理解前面学习的局部变量和全局变量,全局变量的生存状态依赖于整个Javascript程序,局部变量的存在依赖于局部一个程序段,比如一个函数中的局部变量,if语句中的局部变量,for循环结构程序中的局部变量。

  代码第9行到第16行定义的一个具有某种功能的函数,比如执行函数第13行的语句,CPU就会在内存数据区开辟内存空间存储一个新的数组,具体来说数组被存储到了动态内存的堆区,数组的地址arr被存储到栈区,CPU通过栈区数组的地址 可以访问堆区的数据数据,比如arr[0]指针指向数组的第一个元素。这里你可以看出访问堆区的数据要经过一个中间区栈区,说明栈区数据的执行效率高。计算机的硬件资源是有限的,CPU的运算能力是有限的,所以要优化算法,内存的存储空间是有限的, 因此要对内存进行合理分配,比如局部变量分配在栈区可以快速调用,调用结束然后迅速清除释放。堆区的数据清除释放当时和栈区有所不同,栈区数据释放比较简单,只要局部变量的相关程序执行完毕,它就释放。也就是说被释放的数据属于垃圾数据,没有用的 数据,那么堆区的数据如何判断是垃圾数据?堆区的数据的访问是通过栈区的地址,如果栈区的地址不在指向堆区中的数据,那么Javascript的解释器就会把该数据占用的内存区域清空释放,如果是C++和C语言需要程序员手动释放,Javascript语言运行的浏览器 或nodejs平台都会按照一定的规则自动检测堆区数据,一旦发现垃圾数据就会释放,一般不需要程序员手动释放内存,不过有些时候还是需要程序员手动干预,对于简单的工程,浏览器队都会自动管理内存,不过复杂的工程还是需要程序员对内存分配、管理有深入的 认识。

let arr = [];
// arr等于空指针,释放内存
arr = null;

  null前面讲解过,是一个空值,空指针,你可以简单把它理解为0,一般用于释放堆区数据,arr原本指向堆区的数组数据,但是你重新给arr赋值为空指针,不在指向arr数组,该数组占据的内存空间就会被释放。

  对比下面代码,比较基本类型数据与引用类型的数据有什么区别,引用类型的数据在堆区中释放的算法,Javascript程序会对数组等引用数据的引用数量计数,一旦没有引用,就会被清除释放。

    function fun() {
// 声明一个变量a,赋值10
    let a = 10;
// 重新开辟内存空间存放b并把a的值10赋值给b
    let b = a;
// 变量a原来的值10被覆盖重新更改为20,b的值不受影响
    let a = 20;
// 声明数组存放在堆区,数组地址存放在栈区
    let arr1 = [];
// 声明一个变量arr2存储在栈区,作为一个指针,和arr1一样指向同一个数组
    let arr2 = arr1;
// arr2.push(2);和arr1.push(2);作用相同都会改变数组结构
// 语言描述arr1数组和arr2数组指的都是同一个数组
    arr1.push(2);

    let obj = {};
// 变量arr2指向新的数据obj对象
// 注意这不会改变原来的数组,此时arr2指向的数组只是少了一个引用
    let arr2 = obj;
// 注意此时arr1和arr2指向的数组都指向了别的对区数据,数组的引用为0,被清除释放
    let arr1 = obj;
}