javascript的内存管理

577 阅读7分钟

内存的生命周期

在javascript中,当我们创建变量、函数或者其它的时候,js引擎会为它分配内存并在不需要的时候释放内存.

分配内存是在内存中保留空间的过程,释放内存则是释放空间以便于其它目的

每一次创建变量或者声明函数的时候,内存总要经历以下过程

分配内存:

js引擎为我们创建的变量、函数分配所需的内存

使用内存:

使用内存是我们代码中明确进行的工作,对内存的读写只不过是对变量的读写

释放内存:

这个步骤也是由js引擎处理,释放内存之后就可以用于其它目的

堆内存和栈内存

js引擎在两个地方储存数据: 堆内存和栈内存

堆和栈是引擎用于不同目的的两种数据结构

栈:静态内存分配

栈内存是js引擎用来储存静态数据的数据结构,静态数据是在引擎进行编译时知道大小的数据,在js中,这包含原始值(数字,字符串,布尔值、undefined、null)和指向函数和对象的引用

由于引擎知道数据的大小不会变,所以它为每个值分配固定数量的内存

在执行之前分配内存的过程称为静态内存分配

因为引擎为这些值分配了固定数量的内存,所以原始值的大小是有限制的

这些值和整个栈的限制取决于不同的js引擎

所有的值都储存在栈中,因为它们都是原始值

堆:动态内存分配

堆内存是js引擎用于储存对象和函数的空间

与栈内存不同,js引擎不为这些对象分配数量固定的内存. 相反,将根据需要分配更多的空间,这种方式分配内存也称为动态内存分配

下边是两种储存特性的比较:

栈                                                         堆

原始值和引用                        object和function

大小在编译时已知               在运行时知道大小

分配固定数量的内存           每个对象没有限制

example:

  const person = {
    name: 'John',
    age: 24,
  };

js在堆中为这个对象分配内存

  const hobbies = ['hiking', 'reading'];

数组也是对象,所以它被分配在堆内存当中

  let name = 'John'; // 为一个字符串分配内存
  const age = 24; // 为一个数字分配内存
  
  name = 'John Doe'; // 为一个新的字符串分配内存
  const firstName = name.slice(0,4); // 为一个新的字符串分配内存

原始值是不可变的,这意味着js不会改变原始值,而是创建一个新值

在js中的表现

所有的变量首先指向栈,如果它不是原始值,则栈中包含对象的引用

堆内存中没有特定的方式排序,这就是为什么要在栈中保存它的引用

请记住: js在堆中储存对象,在栈中储存原始值和引用

在这张图中可以观察到不同的值是如何储存的,注意 person 和 newPerson 指向同一个对象

垃圾回收(gc)

现在还有最后一步没有完成:释放内存

就像内存分配一样,js引擎也会为我们处理这一步,更具体地说垃圾收集器(gc)负责处理这个问题

一旦js引擎意识到不再需要给定的变量和函数,它就会释放所占用的内存,这样做的主要问题是:无法确定一些内存是否被需要,这意味着没有一种算法能够在过时的那一刻立即收集不再需要的内存

一些算法很好的逼近了这个问题, 在这里将介绍最常用的方法:引用计数垃圾收集和标记清除法

引用计数的垃圾收集

这是解决问题最简单的近似值,它收集没有引用指向它们的对象

视频链接

请注意在最后一帧中只有 hobbies 保存在堆中

循环引用

这个算法的问题在于它没有考虑到循环引用,当一个或者多个对象循环引用,但是不能通过代码访问它们时,就会发生这种情况

  let son = {
    name: 'John',
  };
  
  let dad = {
    name: 'Johnson',
  }
  
  son.dad = dad;
  dad.son = son;
  
  son = null;
  dad = null;

因为 son 和 dad 两个对象相互引用了,所以算法不会释放已分配的内存,但是也无法通过代码访问它们

将它们设置为 null 并不会使引用计数算法意识到它们不能被使用,因为它们都有被引用

标记清除法

标记清除法具有解决循环引用的方案,它不是通过简单的计算给定对象的引用,而是检测它们是否可以从根节点访问

浏览器中的根是 window,node中的根是 global

该算法将不可访问的对象标记为垃圾,然后清除它们,根对象永远不会被清除

这样循环依赖就不是问题了,上边的示例中 dad 和 son 对象不能从根节点访问,因此它们都被标记为垃圾,并收集

自2012年以来,该算法已在所有现代浏览器中实现。 仅对性能和实现进行了改进,而没有对算法的核心思想进行改进。

权衡

自动垃圾回收让我们专注于构建应用程序,而不是因为内存管理浪费时间. 但是仍有一些点需要注意

内存使用

因为算法无法确切知道何时不再需要内存,javascript应用程序可能会比实际使用需要更多的内存.

即使对象被标记为垃圾,也需要垃圾回收器决定何时,以及是否回收内存.

如果需要应用程序尽可能的提高内存使效率,可能需要更低级的语言

性能

垃圾回收算法通常定期运行,以清除未使用的对象.

问题是作为开发者,我们并不知道这将在何时发生,并且大量的垃圾回收需要一定的算力,然而开发者和用户通常不会注意到这种影响

内存泄露

有了内存管理的这些知识,就可以轻松的避免这些情况

全局变量

将数据储存在全局变量中,可能是最常见的内存泄露

例如在浏览器中,如果使用var而不是使用const或let或者省略关键词,js引擎会将变量附加到window对象上

使用function关键词 也会发生相同的情况

  user = getUser();
  var secondUser = getUser();
  function getUser() {
    return 'user';
  }

user secondUser getUser 这三个变量都将被挂载到window上

也可以使用严格模式来避免这种情况

当然不可避免的要使用全局变量,但是要在不使用数据时,释放内存

    window.users = null;

遗忘的计时器和回调

遗忘的计时器和回调会使应用程序内存使用加剧,特别是在spa应用中.

被遗忘的定时器

  const object = {};
  const intervalId = setInterval(function() {
    // 这里使用到的任何东西都不会被回收,直到计时器被清除
    doSomething(object);
  }, 2000);

一旦不再需要务必要清除定时器

  clearInterval(intervalId);

这在spa中尤为重要,当跳转到其它页面时,计时器仍会在运行

被遗忘的回调

假设向一个按钮添加 onclick 事件回调,之后这个元素被删除,在旧的浏览器中不会回收这个回调,但是现在这不是一个问题,不过,当不再需要事件监听的时候,删除它仍然是一个好的习惯

  const element = document.getElementById('button');
  const onClick = () => alert('hi');
  
  element.addEventListener('click', onClick);
  
  element.removeEventListener('click', onClick);
  element.parentNode.removeChild(element);

dom的引用

这种情况和上边被遗忘的回调类似,它发生在使用js储存dom元素时

  const elements = [];
  const element = document.getElementById('button');
  elements.push(element);
  
  function removeAllElements() {
    elements.forEach((item) => {
      document.body.removeChild(document.getElementById(item.id))
    });
  }

当删除任何元素时也要确保它从数组中删除,否则就不能回收这些元素

  const elements = [];
  const element = document.getElementById('button');
  elements.push(element);
  
  function removeAllElements() {
    elements.forEach((item, index) => {
      document.body.removeChild(document.getElementById(item.id));
  
      elements.splice(index, 1);
    });
  }

翻译总结自:felixgerschau.com/javascript-…