js垃圾回收机制,内存泄露和内存溢出,解决闭包产生的内存泄露详解

一、内存的周期和回收机制

分配内存----->使用内存----->释放内存

1.JS 环境中分配的内存有如下声明周期:

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

2.JS 的内存回收 

JS 有自动垃圾回收机制,那么这个自动垃圾回收机制的原理是什么呢? 其实很简单,就是找出那些不再继续使用的值,然后释放其占用的内存。

大多数内存管理的问题都在这个阶段。 在这里最艰难的任务是找到不再需要使用的变量。

①不再需要使用的变量也就是生命周期结束的变量,局部变量,局部变量只在函数的执行过程中存在, 当函数运行结束,变量就没有存在的必要了,没有其他引用(闭包),那么该变量会被回收。

②对于全局变量,很难判断什么时候不用这些变量,无法正常回收,所以全局变量的生命周期直至浏览器卸载页面才会结束,也就是说全局变量不会被当成垃圾回收

因为自动垃圾回收机制的存在,开发人员可以不关心也不注意内存释放的有关问题,但对无用内存的释放这件事是客观存在的。 不幸的是,即使不考虑垃圾回收对性能的影响,目前最新的垃圾回收算法,也无法智能回收所有的极端情况。

二、内存泄露(内存浪费)

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

通俗点就是指由于疏忽或者错误造成程序未能释放已经不再使用的内存,不再用到的内存却没有及时释放,从而造成内存上的浪费。

例子:

使用闭包产生的内存浪费来举例,不懂闭包的可以先去看我的另一篇文章

JS---JS中的闭包详解_Cirrod的博客-CSDN博客

注意点:

new Array()用于创建数组,存放在中,变量arr是对这个数组的引用,指向数组,放在

   <script>
    function fn() {
      //再次强调,arr变量存储在栈中,Array(10000000)的值存储在堆中,
      //Array(10000000)大概占用40mb左右
      let arr = Array(10000000)
      return function () {
        //为了方便显示堆占用内存的差距,console.log(arr[0])这里引用fn函数作用域中的Array(10000000),所以
        //会导致闭包的产生,导致fn函数作用域中的Array(10000000)在堆中的内存无法释放
        console.log(arr[0]);
        //这里返回函数再次返回了一个Array(10000000)数组,如果返回函数使用完不手动清除
        //也会造成内存浪费
        return new Array(10000000)
      }
    }
    let fun = fn();
    let test = fun();
  </script>

在浏览器查看程序占用堆的内存

 

 

ps:可以看到此时堆内存占用81mb,也就是存在两个Array(10000000)

修改代码进行内存释放:

  <script>
    function fn() {
      //再次强调,arr变量存储在栈中,Array(10000000)的值存储在堆中,
      //Array(10000000)大概占用40mb左右
      let arr = Array(10000000)
      return function () {
        //为了方便显示堆占用内存的差距,console.log(arr[0])这里引用fn函数作用域中的Array(10000000),所以
        //会导致闭包的产生,导致fn函数作用域中的Array(10000000)在堆中的内存无法释放
        console.log(arr[0]);
        //这里返回函数再次返回了一个Array(10000000)数组,如果返回函数使用完不手动清除
        //也会造成内存浪费
        return new Array(10000000)
      }
    }
    let fun = fn();
    let test = fun();
    //将返回函数清理掉,释放返回函数的内存,进而解除对fn函数作用域中的Array(10000000)的引用,
    //从而释放fn函数作用域中的Array(10000000)的内存
    fun = null
    //将返回函数中的返回的Array(10000000)清理掉,释放内存
    test = null
  </script>

在浏览器查看程序占用堆的内存 

 

ps:可以看到此时堆内存占用1.4mb,也就是两个Array(10000000)已经被浏览器进行回收

三、内存溢出

当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误。

    for (let index = 0; index < 10000000000; index++) {
      let element = new Array(10000000)
    }

四、常见的js内存泄露

1.意外的全局变量(就是上面所说的全局变量不能被js垃圾回收机制回收)

在js中,一个未声明变量的使用,会在全局对象中创建一个新的变量;在浏览器环境下,全局对象就是window

    function test() {
      names = 'hsq'
    }
    test();
    console.log(names);//hsq

 相当于

    function test() {
      window.names = 'hsq'
    }
    test();
    console.log(window.names);//hsq

解决办法:

①使用js严格模式

未声明但直接赋值的变量将失败

//js文件
"use strict";
function test() {
  names = 'hsq'
}
test();
console.log(names);//报错

②变量使用完后将变量的内存释放

    function test() {
      names = 'hsq'
    }
    test();
    console.log(names);//hsq
    names = null
    console.log(names);//null

2、计时器和回调函数

定时器setinterval或者settimeout在不需要使用的时候,没有被clear定时器无法被内存回收,导致定时器的回调函数其内部依赖的变量都不能被回收,这就会造成内存泄漏。

案例:

  <script>
    let serverData = '后端返回数据';
    window.setInterval(() => {
      let renderer=document.getElementById('renderer');
      if(renderer){
        renderer.innerText=serverData
      }
    },5000)//每5秒更新后端返回的数据
  </script>

图解:

如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器的回调函数回调函数中的依赖也无法回收。在这个案例中定时器的回调函数回调函数里面的的依赖变量serverData )也无法被回收。

解决方案:当不需要interval定时器或者timeout定时器的时候,调用clearinterval或者cleartimeout清除定时器

  <script>
    let serverData = '后端返回数据';
    let timer = window.setInterval(() => {
      let renderer = document.getElementById('renderer');
      if (renderer) {
        renderer.innerText = serverData
      }
      //清除定时器,防止内存泄露
      clearInterval(timer)
    }, 5000)//每5秒更新后端返回的数据
  </script>

3、DOM引用

虽然我们将dom元素已经移除,但是对dom元素的引用没有清除,也会造成内存泄露

案例:

let img=document.getElementById('img')
 img.src='http://image.png'
 document.body.removeChild(img)// 这时我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.

ps:

变量img存储的是dom元素(#img)的引用,我们将dom元素(#img)在文档树中清除掉后,dom元素(#img)已经不存在了,但对dom元素(#img)的引用还存在,也就是变量img还是存储着对dom元素(#img)的引用,也会造成内存泄露

解决方案:

利用null释放内存

 let img=document.getElementById('img')
 img.src='http://image.png'
 document.body.removeChild(img)// 这时我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
 img=null;//清空dom元素的引用

注意点:

另外需要注意的一个点是,对于一个 Dom 树的叶子节点的引用。 举个例子: 如果我们引用了一个表格中的td元素,一旦在 Dom 中删除了整个表格,我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素。 但是事实上,这个 td 元素是整个表格的一个子元素,并保留对于其父元素的引用。 这就会导致对于整个表格的引用都无法进行内存回收。所以我们要小心处理对于 Dom 元素的引用。

 
4、闭包

  闭包是函数运行时候所产生的机制,函数执行会形成一个全新的私有上下文,可以保护里面的私有变量和外界互不干扰(保护机制),当其他上下文占用了当前上下文中的变量,导致当前上下文的变量不被释放,即大家所认为的闭包,需要当前上下文的变量不能被释放,这样私有变量及它的值也不会被释放掉(保存机制),

  下面是比较常见的闭包场景:

在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。 下面这种情况下,闭包也会造成内存泄露:

  <script>
    //fn这个函数作用域访问了另一个函数fn里面的局部变量num,那么函数fn就是一个闭包函数
    function fn() {
      let num = 3
      return function () {
        //console.log(num)语句依赖fn函数的局部num变量,所以会产生闭包,导致num变量不能被内存回收,
        // 因为这里的函数是返回函数,不知道什么时候会再次调用,
        //所以内存需要一直保存num变量,所以不会进行回收
        console.log(num);

      }
    }
    let test = fn();//此时fn函数已经执行,正常的js垃圾回收机制会把fn函数的局部变量num进行回收,但是由于闭包的产生,所以此时不会进行回收处理
    test()//3,返回函数依然可以访问fn函数的局部变量num
    window.setTimeout(() => {
        let test = fn();//此时fn函数已经执行,正常的js垃圾回收机制会把fn函数的局部变量num进行回收,但是由于闭包的产生,所以此时不会进行回收处理
    test()//3,经过3秒返回函数依然可以访问fn函数的局部变量num
    }, 1000)

  </script>

ps:

关键在于,闭包之间是共享作用域的, //console.log(num)语句依赖fn函数的局部num变量,所以会产生闭包,导致num变量不能被内存回收。

解决方案:

参考文章:

JavaScript 闭包 内存泄漏与解决办法_yongzhi1u的博客-CSDN博客_js 闭包内存泄露

函数使用完后,手动释放内存,设置为null

  <script>
    //fn这个函数作用域访问了另一个函数fn里面的局部变量num,那么函数fn就是一个闭包函数
    function fn() {
      let num = 3
      return function () {
        //console.log(num)语句依赖fn函数的局部num变量,所以会产生闭包,导致num变量不能被内存回收,
        // 因为这里的函数是返回函数,不知道什么时候会再次调用,
        //所以内存需要一直保存num变量,所以不会进行回收
        console.log(num);

      }
    }
    let test = fn();//此时fn函数已经执行,正常的js垃圾回收机制会把fn函数的局部变量num进行回收,但是由于闭包的产生,所以此时不会进行回收处理
     test()//3,返回函数依然可以访问fn函数的局部变量num
    test = null;//返回函数运行后,将返回函数的引用(test变量)清除掉,返回函数的引用被清除掉后,返回函数就会被销毁掉,同时取消对fn函数的局部变量num的依赖,解决闭包产生的内存浪费
    window.setTimeout(() => {
     let test = fn();//此时fn函数已经执行,正常的js垃圾回收机制会把fn函数的局部变量num进行回收,但是由于闭包的产生,所以此时不会进行回收处理
    test()//3,经过3秒后返回函数依然可以访问fn函数的局部变量num
    test = null;//返回函数运行后,将返回函数的引用(test变量)清除掉,返回函数的引用被清除掉后,返回函数就会被销毁掉,同时取消对fn函数的局部变量num的依赖,解决闭包产生的内存浪费
    }, 1000)


  </script>

五、如何避免内存泄漏

一个原则:不用的东西,及时归还。(使用完后设置为null)

  1. 减少不必要的全局变量,使用严格模式避免意外创建全局变量。
  2. 在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
  3. 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

参考文章:

JS常见内存泄漏及解决方案解析

「前端进阶」JS中的内存管理 - 知乎

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cirrod

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值