朋友们,美好的一天!
在大多数情况下,作为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);
});
}
希望您发现自己感兴趣的东西。感谢您的关注。