Javascript系列之被忽视的垃圾回收和内存泄露?

235 阅读6分钟

前言

前篇文章已经阐述了内存分配的详情,如果有疑问的可以阅读JavaScript系列之内存分配。下面就让我们看下内存分配后,浏览器在处理内存收集时各种算法和会被我们忽视的问题。

垃圾收集

我们现在知道 JavaScript 如何为各种对象分配内存,但如果我们还记得内存生命周期,就会遗漏最后一步:释放内存。就像内存分配一样,JavaScript 引擎也为我们处理了这一步。更具体地说,负责这一项任务的是垃圾收集器

一旦 JavaScript 引擎识别出不再需要给定的变量或函数,它就会释放它占用的内存。

这样做的主要问题是,是否仍然需要一些内存是一个,这意味着它在确切的时刻就已经过时了。无法判定的问题不能是能够收集所有不再需要的内存的算法,一些算法为该问题提供了一个很好的近似值。我将在本节中讨论最常用的算法:引用计数垃圾收集和标记清除算法

引用计数垃圾收集

引用计数垃圾收集这是最简单的近似值。它收集指向它们的对象。 请注意最后一帧如何仅保留在堆中,因为它是最终具有引用的对象。

周期

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

let son = {
  name: 'John',
};let

 dad = {
  name: 'Johnson',
}.

sondad = dad;
dad.son = son;=

son  null;
dad = null;

因为对象相互引用,算法不会释放分配的内存。我们无法再访问这两个对象。因为它们都有传入引用,计数算法就没法使引用识别出它们不能再使用。

标记扫描算法

mark-and-sweep算法有解决循环依赖的方法。它不是简单地计算对给定对象的引用,而是检测它们是否是从根对象可达,浏览器中的根是Object,而在 NodeJS 中是windowglobal。

作为该算法收集垃圾,它永远不会收集根对象。标记不可到达的对象,然后清扫(收集)它们

这样,循环依赖就不再是问题了。在前面的示例中,两个对象都不能从根到达。因此,它们都将被标记为垃圾并被收集。

自 2012 年以来,在所有现代浏览器中该算法是仅对性能和实现进行了改进,但并未对算法的核心思想本身进行改进。

取舍

自动垃圾收集使我们能够专注于构建应用程序,而不是浪费时间在内存管理上。但是,我们需要注意一些权衡。

内存使用情况

鉴于算法无法知道何时不再需要内存,JavaScript 应用程序可能使用比实际需要更多的内存

即使对象被标记为垃圾,由垃圾收集器决定何时以及是否收集分配的内存。

如果您需要您的应用程序尽可能提高内存效率,您最好使用低级语言。但请记住,这有其自身的一系列权衡。

表现

为我们收集垃圾的算法通常会定期运行以清理未使用的对象。

问题是我们开发人员不知道什么时候会发生。收集大量垃圾或频繁收集垃圾可能会影响性能,因为这样做需要一定的计算能力。

但是,这种影响通常不会被用户或开发人员察觉。

内存泄露

将数据存储在全局变量中可能是最常见的内存泄漏类型。 例如,在浏览器中,如果您使用instead of or — 或者完全省略关键字 — 引擎会将变量附加到对象 用关键字定义的函数也会发生同样的情况

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

所有三个变量都将附加到对象window。usersecondUsergetUser;

除了不小心将变量添加到根之外,在许多情况下您可能是故意这样做的。您当然可以使用全局变量,但请确保在不再需要数据时释放空间。要释放内存,请将全局变量分配给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');.addEventListener('click',

element onClick);.removeEventListener('click',

element 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))
  });
}

当您删除这些元素中的任何一个时,您可能希望确保也从数组中删除该元素。 否则,无法收集这些 DOM 元素。

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);
  });
}

由于每个 DOM 元素也保留对其父节点的引用,您将阻止垃圾收集器收集元素的父元素和子元素

写在结尾

以上就是垃圾回收的所有啦,是不是发现对于内存泄露相关的我们都关注的比较少,希望本文章会给大家在内存泄露上有一个新的看法,以后更注重内存泄露问题。如有不对还请大家积极指出,我也会及时更正哒,感谢大家的认可,如果本文对你有帮助,就帮忙点个赞支持下吧,非常感谢。