学新通技术网

【JavaScript】 垃圾回收(GC)与内存泄漏

juejin 18 1
【JavaScript】 垃圾回收(GC)与内存泄漏

垃圾回收机制(GC)

垃圾回收机制(GC)介绍

垃圾回收机制简称 GC,是 Garbage Collection 的简写(以下我们简称GC)。

GC首先我们需要了解,我们的内存被分为两块区域(栈内存/堆内存)。 对于引用类型数据本身是保存在堆内存,而栈内存中值保存堆内存的数据地址。

比如这样↓

image.png

如果对值类型和引用类型不明白的同学,去(javascript高级篇之深拷贝和浅拷贝的概念 - 掘金 (juejin.cn))这里看完会来就晓得了。很简短。

GC这套引擎的作用就是回收我们散落在堆内存中已经没有指向的变量空间。对没错。GC是只针对堆内存变量回收的一套引擎,只回收堆内存中的数据,根栈内存半毛钱关系没有。

image.png

小伙伴们需要注意一下:每一个浏览器的GC都不一样,GC是一个概念。他不是某一个固定的引擎。曾经有好多人都认为GC是一套统一且固定的引擎。

GC一般的编程语言都是自带的。本身并不需要我们去编写。C,C++除外。 这两个语言相对硬核。不自带GC。在 C/C++ 中,垃圾数据的内存释放需要开发人员手动进行。不过因为他们的内存可以手动管理,所以这两个语言也能做到比其他语言更好的性能优化。

什么是没有指向的变量空间?

上面我们说到,GC是回收内存中没有指向的内存空间的。有些小伙伴可能不明白什么叫没有指向的空间内存。我给大家画了张图。

image.png

obj3这个对象,就属于那种需要被GC回收的变量。在堆内存中存在,确没有在栈内存中被引用。

我们访问一个引用类型变量的流程如下↓

  1. 收到访问指令
  2. 去栈内存中寻找该数据存在于堆内存中的地址
  3. 拿着堆内存地址去堆内存中寻找该数据

如此一来。如果一个数据没有在栈内存中被指向。自然我们永远也不可能访问到它。这样的数据,被我们称为垃圾数据也就是所谓没有指向的内存空间,就会被GC回收并销毁。

为什么要销毁这些没有指向的内存空间

我知道这个问题很无聊,但秉着包教包会的原则,也考虑到底子不大好的小伙伴。还是要讲一下。

我们电脑在运行过程中需要占用大量的内存空间。你电脑上所有的软件在启动的时候都会占比一定的内存。内存越大,我们的执行效率也就越快。

如果我们不去销毁这些没有指向内存空间。那么这些垃圾数据就会一直占着你的内存,数量一旦多起来,原本16G的内存可能就剩个12G了。电脑会越来越卡。

GC销毁他们,就是为了释放更多的内存空间。为电脑提供更好的运行环境。

系统如何区分所谓的"垃圾"?

方法有很多,常用的有两个标记清除算法引用清除算法

标记清除算法

标记清除算法会让所有的变量在进入内存的时候做一个标记。每一次变量的引用改动都会让内存中的变量被重新标记。垃圾变量(失去了所有引用的变量)会被标记为0,而还存在引用的变量会被标记为1。下一次GC开始清理的时候。会将所有标记为0的变量回收。

不过这种方式有一个缺陷,会出现内存碎片。

我画个图给大家理解一下什么是内存碎片。

image.png

被释放的内存位置是不变的。

注意:插入引用类型的数据(比如对象/数组)它们需要一组连续的内存空间

标记清除算法更像是拦腰截断,并不会重新排序内存空间。

后来补充了标记整理算法。也就是可以将一次内存回收后的所有变量集中到一个地方。以腾出更多的内存空间。

image.png

引用清除算法

这个很好理解。

简单来说。就是变量没被引用一次则该变量引用计数+1,取消一次引用则该变量引用技术-1。如果对象的引用计数为0。则马上回收变量。

例如👇

image.png

内存泄漏

内存泄漏:内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

我们把上面这段官方回答翻译以下就是: 有一些内存我们已经使用完毕,后续也不再需要他,他应该被销毁并腾出那一块内存空间。但是他没有。这就是内存泄漏。

内存泄漏和我们上文提到的垃圾回收机制息息相关。其实,本质上,就是有些内存处于意外情况规避了垃圾回收机制。没有被GC引擎成功回收,而造成了这一块无用的内存一直被占用。也就造成了内存泄漏。

在javascript中造成内存泄漏的原因有以下几点:

意外的全局变量

注意,并不是说全局变量都会内存泄漏。如果我们愿意,使用 变量 = null 还是可以释放掉的。

只是全局变量既然为在全局使用。我们一般情况下是不会手动去释放。

总的来说:全局变量在不手动释放的情况下,会在程序运行时一直存在。所以我们需要避免一些全局变量被意外的创建

以下几种情况可能会在我们意料之外创建全局变量;

未声明的变量

{
    const fun = () => {
        num = 1;  //注意,使用变量前应该先使用var/let/const声明,但是这里没有
    }

 }

这些未声明的变量会被默认挂载到全局中 也就是window.num。比如👇

image.png

使用不当的this

两种情况下,使用this指向的变量是可能成为全局变量的。

1.全局函数👇

 function fun(){
            //全局函数,this指向的就是window对象,所以以下写法 === window.str = ""
            this.str = "别人的基金和我的基金: 别人小涨,我也想; 别人小跌,我腰斩; 别人大涨,我不涨; 别人  大跌,我破产。"
         }

         fun();

2.箭头函数中的this

这个出现的概率会稍微大一点。

箭头函数我想大家应该都熟悉,箭头函数没有自己的this指向,所以如果某一个箭头函数中使用了this 并且该函数的父级环境是window的话。就会变成window对象的元素。

比如👇

 const obj = {
            look:"",
            fun:() => {
                this.look = "昨天晚上做梦,梦到我的基金亏了。今早打开一看,果然亏了。"
            }
         }

         //或许有些童鞋会认为以下会赋值给obj.look的内容。并不会。
         //因为箭头函数没有自己的this指向,箭头函数无法指向obj,只能指向obj的父级(也就是window)
         obj.fun()

image.png

被遗忘的定时器

我想大多数小伙伴应该和以前的我一样。经常不去取消一些已经无用的定时器、反正放那也不会造成什么影响嘛。看起来是如此。但实际上没有取消且无用的定时器会占用我们的内存。

比如👇

    const obj = {
        str:"前天刚发工资。以下是我裤兜里剩下的money",
        num:-1,
        look:"点个赞呗"
    }
    
    //尽管我们这里只引用了obj.str,实际上obj中其他的属性也不会被释放
    
    setInterval(
    () => {console.log(obj.str)}
    ,1000)

该定时器内部所有的变量也都会被保存,一直到我们的程序被关闭为止。当某一个定时器内部引用的对象比较多比较大的时候。这样的泄漏可能会给项目带来一定程度上的问题。

所以,为了内存不泄露,为了项目稳定,为了不加班改bug。我们需要善用clearInterval()clearTimeout()

不正当的闭包

能点进这篇文章的小伙伴,对闭包应该都是很熟悉的。闭包是最广为人知的会造成内存泄漏的元凶之一。

闭包,也就是引用其他函数的变量。并保持该变量的状态(也就是不让GC回收);一般我们为了减少和全局变量的耦合,提高函数的聚合性会使用。

值得注意的是,并不是所有的闭包都会造成内存泄漏。如果闭包在使用完毕之后可以得到释放的话。就不算内存泄漏。比如👇

   const fun = () => {
     const a = 1;
     return () => {
      console.log(a);
     }
  }
  
  let fn = fun();
  
  fn(); //输出a
   
  fn = null;//使用完毕之后手动释放内存

以上这样的闭包,就不会造成内存泄漏。

关于闭包,没错,闭包会造成内存泄漏。但是只要我们的函数别放在某一个循环和定时器里面自动执行。一般情况下,就这几个内存占有量。不会对项目造成任何伤害。而且这些在内存中的变量会在组件被销毁的时候自动销毁。

不要大量的使用闭包(实际上使用闭包的场景也不多),不要再重复执行的函数中使用闭包(比如循环和定时器)。其他的闭包我们不去特定的释放。也没什么关系。

引用已经删除的节点

简单来说,就是使用了document.getElementById()等方法获取了一些变量并赋值给了某一个变量。

例如这种↓ image.png

好在使用react/vue等声明式框架之后,一般我们不会去使用这种命令式语句。react/vue也不建议我们直接去操作DOM元素。

如果有一天我们不得已再次需要手动操作DOM,记得在节点被删除的时候,顺便将节点的引用释放一下。

释放节点的方法👇

    let node = document.getElementById("id"); //这是引用节点
    
    node = null; //置为null就是释放节点引用

最后

GC是一个概念,用于回收存在于堆内存中的垃圾数据。一般的高级语言有自带。但c/c++没有。浏览器可能会对自带的GC做一些优化。

GC只回收堆内存。

GC的最常用的两个算法是:**引用

导致内存泄漏的原因有很多。包括但不限于👇

  • 意外的全局变量
  • 被遗忘的定时器
  • 使用不当的闭包
  • 引用已经删除的节点

能看到这里,你真的很有耐心。

祝你发财,祝你被爱,祝你好运常在。

本文出至:学新通技术网

标签: