JavaScript内存管理





朋友们,美好的一天!



在大多数情况下,作为JavaScript开发人员,我们无需担心使用内存。引擎为我们做到了。



但是,有一天您将遇到一个称为“内存泄漏”的问题,只有通过了解JavaScript中的内存分配方式才能解决该问题。



在本文中,我将解释内存分配和垃圾回收的工作方式,以及如何避免与内存泄漏相关的一些常见问题。



记忆体生命周期



创建变量或函数时,JavaScript引擎会为其分配内存,并在不再需要它时释放它。



分配内存是在内存中保留特定空间的过程,释放内存就是释放该空间,以便可以将其用于其他目的。



每次创建变量或函数时,内存都会经历以下阶段:







  • 内存分配-引擎自动为创建的对象分配内存
  • 内存使用情况-读取和写入数据到内存无非就是从变量写入和读取数据
  • 释放内存-引擎也会自动执行此步骤。释放内存后,即可将其用于其他目的。


堆放



下一个问题是:记忆是什么意思?数据实际存储在哪里?



引擎有两个这样的地方:堆和栈。堆和堆栈是引擎用于不同目的的数据结构。



堆栈:静态内存分配







该示例中的所有数据都存储在堆栈中,因为它是基元,



堆栈是用于存储静态数据的数据结构。静态数据是在代码的编译阶段引擎知道大小的数据。在JavaScript中,此类数据是原语(字符串,数字,布尔值,未定义和null)以及指向对象和函数的引用。



由于引擎知道数据的大小不会改变,因此它为每个值分配固定的内存大小。在执行代码之前分配内存的过程称为静态内存分配。由于引擎分配了固定大小的内存,因此此大小有某些限制,这与浏览器高度相关。



堆:动态内存分配



堆用于存储对象和函数。与堆栈不同,引擎不会为对象分配固定大小的内存。根据需要分配内存。这种内存分配称为动态。这是一个小的比较表:



叠放
原始值和参考 对象和功能
大小在编译时已知 大小在运行时已知
固定内存分配 每个对象的内存大小不受限制


示例



让我们看几个例子。



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


引擎在堆上为此对象分配内存。但是,属性值存储在堆栈中。



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


数组是对象,因此它们存储在堆中



  let name = "John";
  const age = 24;

  name = "John Doe";
  const firstName = name.slice(0, 4);


基元是不可变的。这意味着JavaScript不会更改原始值,而是创建一个新值。



链接



所有变量都存储在堆栈中。对于非原始值,堆栈将对堆中对象的引用存储起来。堆上的内存混乱。这就是为什么我们需要堆栈上的链接。您可以将链接视为地址,将对象视为位于特定地址的房屋。







在上图中,我们可以看到如何存储各种值。注意person和newPerson指向同一个对象



示例



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


这将在堆上创建一个新对象,并在堆栈上创建对该对象的引用



垃圾收集



一旦引擎注意到不再使用变量或函数,它将释放它占用的内存。



实际上,释放未使用的内存的问题是无法解决的:没有完美的算法可以解决它。



在本文中,我们将研究两种迄今为止提供最佳解决方案的算法:引用计数垃圾回收以及标记和清除。



通过引用计数进行垃圾收集



这里的一切都很简单-没有参考点从内存中删除的对象。让我们来看一个例子。线代表链接。







请注意,只有“爱好”对象保留在堆上,因为在堆栈上仅引用了该对象。



循环链接



这种垃圾收集方法的问题是无法定义循环引用。在这种情况下,两个或更多对象指向彼此,但没有外部参照。那些。这些对象不能从外部访问。



  const son = {
    name: "John",
  };

  const dad = {
    name: "Johnson",
  };

  son.dad = dad;
  dad.son = son;

  son = null;
  dad = null;






由于对象“ son”和“ dad”相互引用,因此引用计数算法将无法释放内存。但是,这些对象不再对外部代码可用。



标记和清理算法



该算法解决了循环引用的问题。它无需计算指向对象的引用,而是从根对象确定对象的可用性。根对象是浏览器中的“窗口”对象,或者是Node.js中的“全局”对象。







该算法将对象标记为不可访问并删除它们。因此,循环引用不再是问题。在以上示例中,从根对象无法访问对象“ dad”和“ son”。它们将被标记为垃圾并被删除。自2012年以来,该算法已在所有现代浏览器中实现。从那时起所做的改进是关于实现和性能的改进,而不是算法的核心思想。



妥协



自动垃圾收集使我们能够专注于构建应用程序,而不会浪费时间进行内存管理。但是,一切都是有代价的。



内存使用情况



由于算法需要花费一些时间来确定不再使用内存,因此JavaScript应用程序倾向于使用比实际需要更多的内存。



即使对象被标记为垃圾,收集器也必须决定何时收集它们,以免阻塞程序流。如果希望应用程序在内存使用方面尽可能地高效,那么最好使用较低级别的编程语言。但是请记住,这种语言有其自身的权衡。



性能



垃圾回收算法会定期运行以清理未使用的对象。问题是作为开发人员的我们并不确切知道何时会发生这种情况。大量垃圾回收或频繁垃圾回收会影响性能,因为它需要一定数量的处理能力。但是,这种情况通常不会被用户和开发人员注意到。



内存泄漏



让我们快速看一下最常见的内存泄漏问题。



全局变量



如果在不使用关键字之一(var,let或const)的情况下声明变量,则该变量将成为全局对象的属性。



  users = getUsers();


以严格模式执行代码可以避免这种情况。



有时我们故意声明全局变量。在这种情况下,为了释放此类变量占用的内存,必须为它分配值“ null”:



  window.users = null;


被遗忘的计时器和回调



如果您忘记了计时器和回调,则应用程序的内存使用量可能会急剧增加。注意,特别是在创建单页应用程序(SPA)时,其中动态添加了事件处理程序和回调。



被遗忘的计时器



  const object = {};
  const intervalId = setInterval(function () {
    // ,   ,      ,
    //   ,     
    doSomething(object);
  }, 2000);


上面的代码每2秒运行一次该函数。如果您不再需要计时器,则必须通过以下方式取消计时器:



  clearInterval(intervalId);


这对于SPA尤其重要。即使您切换到不使用计时器的另一个页面,它也会在后台运行。



被遗忘的回调



假设您为以后单击删除的按钮单击注册了处理程序。实际上,这不再是问题,但是仍然建议删除不再需要的处理程序:



  const element = document.getElementById("button");
  const onClick = () => alert("hi");

  element.addEventListener("click", onClick);

  element.removeEventListener("click", onClick);
  element.parentNode.removeChild(element);


DOM之外的链接



此内存泄漏与之前的内存泄漏相似,是在JavaScript中存储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);
    });
  }


希望您发现自己感兴趣的东西。感谢您的关注。



All Articles