JavaScript 的内存管理(一)

一、内存

Javascript 程序的运行,不管是依赖于浏览器还是 Node.js 环境,最终的运行都依赖于操作系统,而操作系统的运行依赖于计算机硬件资源。使用 Javascript 语言开发的程序最终会变成一条条指令和数据,要依赖于计算机硬件的执行和存储。

比如两个变量执行加法运算, 就需要 CPU 运算单元的加法器提供支持,而参与运算数据的存储就要占用内存空间。

当然,计算机的硬件资源(CPU 的运算资源和缓存空间、内存条的内存空间等等)是有限的,不可能无限制的使用。所以,程序运行的时候产生的一些没用的中间数据,要及时的清理掉,释放出空间,留出的空间用以存储其他数据,这就是所谓的垃圾回收

举个简单的例子:

我们要计算 1+2+4 的结果,那么 1+2 的和 3 就是一个临时数据,只用到它来加 4 得到最终结果 7 存在内存中,之后 3 就不会再占用具体的内存空间了。

二、内存的生命周期

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存

  2. 使用分配到的内存(读、写)

  3. 不需要时将其释放 / 归还

C 语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()free()。相反,JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时自动释放。 释放的过程称为垃圾回收。这也让 JavaScript开发者错误的感觉可以不关心内存管理。

二、内存分配

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

javascript-memory-management.png

以下面的代码为例,分别对几个名字做一下简单介绍:

//常量 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 都有自己硬件层面的指令集)。
    • 代码区存放的是 forifwhile 等程序结构经过解析编译处理后存放在内存中的程序指令,这些程序指令可以控制 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';
      
      img
    • 上面的代码中有一个函数,函数里初始化了一个数组,这个时候,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: '陈鑫' };
      
      img

看下面的示例代码,比较基本类型数据与引用类型的数据有什么区别。

引用类型的数据在堆区中释放的算法,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();
发布了31 篇原创文章 · 获赞 11 · 访问量 2714

猜你喜欢

转载自blog.csdn.net/Deepspacece/article/details/104339683