zoukankan      html  css  js  c++  java
  • JavaScript 内存管理

    生命周期中的每一步大概的说明:

    • 分配内存 — 内存是被操作系统分配,这允许程序使用它。在低级语言中(例如C),这是一个作为开发者需要处理的显式操作。在高级语言中,然而,这些操作都代替开发者进行了处理。
    • 使用内存。 实际使用之前分配的内存,通过在代码操作变量对内在进行读和写。
    • 释放内存 。不用的时候,就可以释放内存,以便重新分配。与分配内存操作一样,释放内存在低级语言中也需要显式操作。

    想要快速的了解堆栈和内存的概念,可以阅读本系列第一篇文章。

    什么是内存?内存大概是怎么样工作?

    在硬件中,电脑的内存包含了大量的触发电路,每一个触发电路都包含一些<span >能够储存1位数据的</span>晶体管。触发器通过 唯一标识符 来寻址,从而可以读取和覆盖它们。因此,从概念上来讲,可以认为电脑内存是一个巨大的可读写阵列。

    人类不善于把我们所有的思想和算术用位运算来表示,我们把这些小东西组织成一个大家伙,这些大家伙可以用来表现数字:8位是一个字节。字节之上是字(16位、32位)。

    许多东西被存储在内存中:

    1. 所有的变量和程序中用到的数据;
    2. 程序的代码,包括操作系统的代码。

    编译器和操作系统共同工作帮助开发者完成大部分的内存管理,但是我们推荐你了解一下底层到底发生了什么。

    编译代码的时候,编译器会解析原始数据类型,提前计算出它们需要多大的内存空间。然后将所需的数量分配在 栈空间 中。之所以称为栈空间,是因在函数被调用的时候,他们的内存被添加在现有内存之上(就是会在栈的最上面添加一个栈帧来指向存储函数内部变量的空间)。终止的时候,以LIFO(后进先出)的顺序移除这些调用。

    例如:

    1.  
      int n; // 4字节
    2.  
      int x[4]; // 4个元素的数组,每个元素4字节
    3.  
      double m; // 8字节

    编译器马上知道需要内存

    4 + 4 × 4 + 8 = 28字节。

    这是当前整型和双精度的大小。大约20年以前,整型通常只需要2个字节,双精度需要4个字节,你的代码不受基础数据类型大小的限制。

    编译器会插入与操作系统交互的代码,来请求栈中必要大小的字节来储存变量。

    在上面的例子中,编辑器知道每个变量准确的地址。事实上,无论什么时候我们写变量 n ,将会在内部被翻译成类似“memory address 4127963”的语句。

    注意,如果我们尝试访问 x[4] 的内存(开始声明的x[4]是长度为4的数组, x[4] 表示第五个元素),我们会访问m的数据。那是因为我们正在访问一个数组里不存在的元素,m比数组中实际分配内存的最后一个元素 x[3] 要远4个字节,可能最后的结果是读取(或者覆盖)了 m 的一些位。这肯定会对其他程序产生不希望产生的结果。

    当函数调用其他函数的时候,每一个函数被调用的时候都会获得自己的栈块。在自己的栈块里会保存函数内所有的变量,还有一个程序计数器会记录变量执行时所在的位置。当函数执行完之后,会释放它的内存以作他用。

    动态分配

    不幸的是,事情并不是那么简单,因为在编译的时候我们并不知道一个变量将会需要多少内存。假设我们做了下面这样的事:

    1.  
      int n = readInput(); //读取用户的输入
    2.   ...
    3. //创建一个有n个元素的数组

    编译器不知道这个数组需要多少内存,因为数组大小取决于用户提供的值。

    因此,此时不能在栈上分配空间。程序必须在运行时向操作系统请求够用的空间。此时内存从 堆空间 中被分配。静态与动态分配内存之间的不同在下面的表格中被总结出来:

    静态分配内存与动态分配内存的区别。

    JavaScript中的内存分配

    现在我们来解释JavaScript中的第一步( 分配内存 )是如何工作的。

    JavaScript在开发者声明值的时候自动分配内存。

    1.  
      var n = 374; // 为数值分配内存
    2.  
      var s = 'sessionstack'; //为字符串分配内存
    3.  
       
    4.  
      var o = {
    5.  
      a: 1,
    6.  
      b: null
    7.  
      }; //为对象和它包含的值分配内存
    8.  
       
    9.  
      var a = [1, null, 'str']; //为数组和它包含的值分配内存
    10.  
       
    11.  
      function f(a) {
    12.  
      return a + 3;
    13.  
      } //为函数(可调用的对象)分配内存
    14.  
       
    15.  
      //函数表达式也会分配一个对象
    16.  
      someElement.addEventListener('click', function() {
    17.  
      someElement.style.backgroundColor = 'blue';
    18.  
      }, false);
    19.  
       
    20.  
      //一些函数调用也会导致对象分配
    21.  
      `var d = new Date(); // allocates a Date object` //分配一个Date对象的内存
    22.  
       
    23.  
      `var e = document.createElement('div'); //分配一个DOM元素的内存
    24.  
       
    25.  
      //方法可以分配新的值或者对象
    26.  
       
    27.  
      var s1 = 'sessionstack';
    28.  
      var s2 = s1.substr(0, 3); //s2是一个新的字符串
    29.  
      // 因为字符串是不可变的
    30.  
      // JavaScript可能决定不分配内存
    31.  
      // 而仅仅存储 0-3的范围
    32.  
       
    33.  
      var a1 = ['str1', 'str2'];
    34.  
      var a2 = ['str3', 'str4'];
    35.  
      var a3 = a1.concat(a2);
    36.  
      //新的数组有4个元素是a1和a2连接起来的。

    在JavaScript中使用内存

    在JavaScript中使用被分配的内存,本质上就是对内在的读和写。

    比如,读、写变量的值或者对象的属性,抑或向一个函数传递参数。

    内存不在被需要时释放内存

    大部分的内存管理问题都在这个阶段出现。

    这里最难的任务是找出这些被分配的内存什么时候不再被需要。这常常要求开发者去决定程序中的一段内存不在被需要而且释放它。

    高级语言嵌入了一个叫 垃圾回收 的软件,它的工作是跟踪内存的分配和使用,以便于发现一些内存在一些情况下不再被需要,它将会自动地释放这些内存。

    不幸的是,这个过程是一个近似的过程,因为一般关于知道内存是否是被需要的问题是不可判断的(不能用一个算法解决)。

    大部分的垃圾回收器会收集不再被访问的内存,例如指向它的所有变量都在作用域之外。然而,这是一组可以收集的内存空间的近似值。因为在任何时候,一个内存地址可能还有一个在作用域里的变量指向它,但是它将不会被再次访问。

    垃圾收集

    由于找到一些内存是否是“不再被需要的”这个事实是不可判定的,垃圾回收的实现存在局限性。本节解释必要的概念去理解主要的垃圾回收算法和它们的局限性。

    内存引用

    垃圾回收算法依赖的主要概念是 引用。

    在内存管理的语境下,一个对象只要显式或隐式访问另一个对象,就可以说它引用了另一个对象。例如,JavaScript对象引用其Prototype( 隐式引用 ),或者引用prototype对象的属性值( 显式引用 )。

    在这种情况下,“对象”的概念扩展到比普通JavaScript对象更广的范围,并且还包含函数作用域。(或者global 词法作用域 )

    词法作用域定义变量的名字在嵌套的函数中如何被解析:内部的函数包含了父级函数的作用域,即使父级函数已经返回。

    引用计数垃圾回收

    这是最简单的垃圾回收算法。 一个对象在没有其他的引用指向它的时候就被认为“可被回收的”。

    看一下下面的代码:

    1.  
      var o1 = {
    2.  
      o2: {
    3.  
      x: 1
    4.  
      }
    5.  
      };
    6.  
       
    7.  
      //2个对象被创建
    8.  
      /'o2'被'o1'作为属性引用
    9.  
      //谁也不能被回收
    10.  
       
    11.  
      var o3 = o1; //'o3'是第二个引用'o1'指向对象的变量
    12.  
       
    13.  
      o1 = 1; //现在,'o1'只有一个引用了,就是'o3'
    14.  
      var o4 = o3.o2; // 引用'o3'对象的'o2'属性
    15.  
      //'o2'对象这时有2个引用: 一个是作为对象的属性
    16.  
      //另一个是'o4'
    17.  
       
    18.  
      o3 = '374'; //'o1'原来的对象现在有0个对它的引用
    19.  
      //'o1'可以被垃圾回收了。
    20.  
      //然而它的'o2'属性依然被'o4'变量引用,所以'o2'不能被释放。
    21.  
       
    22.  
      o4 = null; //最初'o1'中的'o2'属性没有被其他的引用了
    23.  
      //'o2'可以被垃圾回收了

    循环引用创造麻烦

    在涉及循环引用的时候有一个限制。在下面的例子中,两个对象被创建了,而且相互引用,这样创建了一个循环引用。它们会在函数调用后超出作用域,应该可以释放。然而引用计数算法考虑到2个对象中的每一个至少被引用了一次,因此都不可以被回收。

    1.  
      function f() {
    2.  
      var o1 = {};
    3.  
      var o2 = {};
    4.  
      o1.p = o2; // o1 引用 o2
    5.  
      o2.p = o1; // o2 引用 o1. 形成循环引用
    6.  
      }
    7.  
       
    8.  
      f();

    标记清除算法

    为了决定一个对象是否被需要,这个算法用于确定是否可以找到某个对象。

    这个算法包含以下步骤。

    1. 垃圾回收器生成一个根列表。根通常是将引用保存在代码中的全局变量。在JavaScript中,window对象是一个可以作为根的全局变量。
    2. 所有的根都被检查和标记成活跃的(不是垃圾),所有的子变量也被递归检查。所有可能从根元素到达的都不被认为是垃圾。
    3. 所有没有被标记成活跃的内存都被认为是垃圾。垃圾回收器就可以释放内存并且把内存还给操作系统。

    上图就是标记清除示意。

    这个算法就比之前的(引用计算)要好些,因为“一个对象没有被引用”导致这个对象不能被访问。相反,正如我们在循环引用的示例中看到的,对象不能被访问到,不一定不存在引用。

    2012年起,所有浏览器都内置了标记清除垃圾回收器。在过去几年中,JavaScript垃圾回收领域中的所有改进(代/增量/并行/并行垃圾收集)都是由这个算法(标记清除法)改进实现的,但并不是对垃圾收集算法本身的改进,也没有改变它确定对象是否可达这个目标。

    循环引用不再是问题

    在上面的例子中(循环引用的那个),在函数执行完之后,这个2个对象没有被任何可以到达的全局对象所引用。因此,他们将会被垃圾回收器发现为不可到达的。

    尽管在这两个对象之间有相互引用,但是他们不能从全局对象上到达。

    垃圾回收器的反常行为

    尽管垃圾回收器很方便,但是他们有一套自己的方案。其中之一就是不确定性。换句话说,GC是不可预测的。你不可能知道一个回收器什么时候会被执行。这意味着程序在某些情况下会使用比实际需求还要多的内存。在其他情况下,在特别敏感的应用程序中,可能会出现短停顿。尽管不确定意味着不能确定回收工作何时执行,但大多数GC实现都会在分配内存的期间启动收集例程。如果没有内存分配,大部分垃圾回收就保持空闲。参考下面的情况。

    1. 执行相当大的一组分配。
    2. 这些元素中的大部分(或者所有的)都被标记为不可到达的(假设我们清空了一个指向我们不再需要的缓存的引用。)
    3. 没有更多的分配被执行。

    在这种情况下,大多数垃圾回收实现都不会做进一步的回收。换句话说,尽管这里有不可达的引用变量可供回收,回收器也不会管。结果会占用比通常情况下更多的内存。

  • 相关阅读:
    Codeforces 1255B Fridge Lockers
    Codeforces 1255A Changing Volume
    Codeforces 1255A Changing Volume
    leetcode 112. 路径总和
    leetcode 129. 求根到叶子节点数字之和
    leetcode 404. 左叶子之和
    leetcode 104. 二叉树的最大深度
    leetcode 235. 二叉搜索树的最近公共祖先
    450. Delete Node in a BST
    树的c++实现--建立一棵树
  • 原文地址:https://www.cnblogs.com/FACESCORE/p/9418165.html
Copyright © 2011-2022 走看看