关于前端:七垃圾回收机制

2次阅读

共计 5553 个字符,预计需要花费 14 分钟才能阅读完成。

前言

本篇章讲述对于垃圾回收机制、内存透露以及堆栈溢出的相干常识,并理解如何通过工具定位排查内存透露状况,实现性能优化。

面试答复

1. 垃圾回收机制:垃圾回收机制就是周期性地找出不再持续应用的变量,开释其内存。那么通常相干的问题就是内存透露问题,起因就是被占用的内存因为程序起因无奈开释,造成节约,如果内存透露过多的话,会使程序无奈申请到内存,呈现程序迟缓甚至解体的状况,那么通常的解决办法就是把这些隐患毁灭在开发阶段,比方防止意外的全局变量、及时敞开定时器、开释 DOM 援用、开释监听事件以及缩小闭包的应用。

知识点

1. 垃圾回收机制

垃圾回收机制即周期性地找出不再持续应用的变量,开释其内存。判断变量是否不再应用的条件是通过标签判断,标识无用变量的策略有以下的 2 种形式。

1. 标记革除

绝大多数浏览器采纳的垃圾收集机制均是通过标记革除的形式,它次要分为两个阶段,标记阶段和革除阶段,判断规范是看这个对象 是否可到达

  • 标记阶段:垃圾收集器会从根对象(Window 对象)登程,扫描所有能够涉及的对象,这就是所谓的 可到达
  • 革除阶段:在扫描的同时,根对象无奈涉及(不可到达)的对象,就是被认为不被须要的对象,就会被当成垃圾革除。

    function changeName(){var obj1={};
      var obj2={};
      
      obj1.target=obj2;
      obj2.target=obj1;
      obj1.age=15;
      console.log(obj1.target);
      console.log(obj2.target);
    }
    
    changeName();

    在函数执行结束之后,函数的申明周期完结,那么当初,从 Window 对象  登程,obj1 和 obj2 都会被垃圾收集器标记为 不可到达(因为办法曾经执行过,扫描就达到不了办法外部,天然也就扫描不到 obj 对象),这样子的状况下,相互援用的状况也会迎刃而解。

2. 援用计数

援用计数法,就是变量援用的次数。你能够认为它就是对以后变量所援用次数的形容。

var obj={name:'jack'}

当给 obj 赋值的同时,其实就创立了一个指向该变量的援用,援用计数为 1,在援用计数法的机制下,内存中的每一个值都会对应一个援用计数。而当咱们给 obj 赋值为 null 时,这个变量就变成了一块没用的内存,那么此时,obj 的援用计数将会变成 0,所有援用计数为 0 的变量都将会被垃圾收集器所回收,而后 obj 所占用的内存空间将会被开释。

2. 内存透露

内存透露(Memory Leak)是指程序中已动态分配的堆内存因为某种原因程序未开释或无奈开释,造成零碎内存的节约,内存泄露过多的话,就会导致前面的程序申请不到内存,因而内存泄露会导致外部内存溢出,导致程序运行速度减慢甚至零碎解体等严重后果。

1. 意外的全局变量

在 JavaScript 中并未严格定义对未声明变量的解决形式,即便在部分函数作用域中仍旧可能定义全局变量,这种意外的全局变量可能会存储大量数据,且因为其是可能通过全局对象例如 window 可能拜访到的,所以进行内存回收时不认为其是须要回收的内存而始终存在,只有在窗口敞开或者刷新页面时才可能被开释,造成意外的内存透露。

// 未声明变量在全局环境创立,通过全局对象拜访
function test() {
    a ='1111'
    console.log('test=====',window.a)
}
test()  
解决形式
  • 应用严格模式(”use strict”),应用 let const 来定义变量,严格模式下定义未声明变量在会抛出谬误;
  • 缩小创立全局变量,如果必须应用全局变量存储大量数据,确保应用完当前把他设置为 null 或者从新定义。

    // 必须要用的状况下, 手动开释全局变量的内存
    window.a = null
    delete window.a

2. 被忘记的定时器和回调函数

当不须要 setInterval 或者 setTimeout 时,定时器没有被 clear,定时器的回调函数以及外部依赖的变量都不能被回收,造成内存透露。

<button> 开启定时器 </button>
<script>
    function fn1() {let largeObj = new Array(100000)
        setInterval(() => {let myObj = largeObj}, 1000)
    }
    document.querySelector('button').addEventListener('click', function () {fn1()
    })
</script>

这段代码是在点击按钮后执行 fn1 函数,fn1 函数内创立了一个很大的数组对象 largeObj,同时创立了一个 setInterval 定时器,定时器的回调函数只是简略的援用了一下变量 largeObj,然而从 Chrome devTools 查看还是能看出内存透露。起因其实就是因为 setInterval 的回调函数内对变量 largeObj 有一个援用关系,而定时器始终未被革除,所以变量 largeObj 的内存也天然不会被开释。

验证形式:关上控制台,点击 Performacne,点击圆点,每个一段时间点击按钮,如果存在内存透露的状况会呈现这种阶梯状

失常的则是单个,如下图。

解决形式

设置敞开条件,应用 clearInterval、clearTimeout 开释

3. 拆散的 DOM 节点

假如你手动移除了某个 dom 节点,本应开释该 dom 节点所占用的内存,但却因为忽略导致某处代码仍对该被移除节点有援用,最终导致该节点所占内存无奈被开释,例如这种状况:

<div id="root">
    <div class="child"> 我是子元素 </div>
    <button> 移除 </button>
</div>
<script>
    let btn = document.querySelector('button')
    let child = document.querySelector('.child')
    let root = document.querySelector('#root')
    
    btn.addEventListener('click', function() {root.removeChild(child)
    })
</script>

该代码所做的操作就是点击按钮后移除.child 的节点,尽管点击后,该节点的确从 dom 被移除了,但全局变量 child 仍对该节点有援用,所以导致该节点的内存始终无奈被开释。

解决形式

改变很简略,就是将对.child 节点的援用挪动到了 click 事件的回调函数中,那么当移除节点并退出回调函数的执行上文后就会主动革除对该节点的援用,那么天然就不会存在内存透露的状况了。

    <div id="root">
        <div class="child"> 我是子元素 </div>
        <button> 移除 </button>
    </div>
    <script>
        let btn = document.querySelector('button')
        btn.addEventListener('click', function () {let child = document.querySelector('.child')
            let root = document.querySelector('#root')
            root.removeChild(child)
        })
    </script>

4. 闭包使用不当

上面的例子中,在 test 函数执行上下文后,该上下文中的变量 a 本应被当作垃圾数据给回收掉,但因 test 函数最终将变量 a 返回并赋值给全局变量 res,其产生了对变量 a 的援用,所以变量 a 被标记为流动变量并始终占用着相应的内存,假如变量 res 后续用不到,这就算是一种闭包使用不当的例子。

<button onclick="myClick()">ddd</button>
<script>
    function test() {let a = [1,2,3] 
        return a
    }
    var res = []
    function myClick() {res.push(test())
    }
</script>
解决形式
// 让不在须要的函数或者变量等于 null
test = null

3. 堆栈溢出

堆栈溢出:每次执行 JavaScript 代码时,都会调配肯定尺寸的栈空间(Windows 零碎中为 1M),每次办法调用时都会在栈里贮存肯定信息(如参数、局部变量、返回值等等),这些信息再少也会占用肯定空间,如果存在较多的此类空间,就会超过线程的栈空间了。堆栈溢出很可能由有限递归产生,但也可能仅仅是过多的堆栈层级。
说白了就是就是不顾堆栈中调配的部分数据块大小,向该数据块写入了过多的数据,导致数据越界,后果笼罩了别的数据。如下示例,便会报错,这是因为过多的函数调用,导致调用堆栈无奈包容这些调用的返回地址,个别在递归中产生。

// 例子,test 在 ES6 环境不会报错,因为有尾调用优化。function test(num){if(num===0) return true
    if(num===1) return false
    return test(Math.abs(num-2))
}
console.log(test(1000000000000))

// 阶乘,若用递归实现,层级不能过深,比方 10 能够、100000 不能够
const factorial = n => n <= 1 ? 1 : n * factorial(n - 1)
factorial(100000)

// 斐波那契数列也一样
const fibonacci = n => n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(100000)

// 都会产生 Uncaught RangeError: Maximum call stack size exceeded 谬误。

解决形式:

1. 递归改为循环

优化原理:所有运算均在一个执行上下文中执行,不必生成额定的上下文。

function newTest(num){if(num===0) return true
    if(num===1) return false
    let result 
    while(Math.abs(num)-2>=0){num = Math.abs(num)-2
        if(num===0){result = true}
        if(num===1) {result =false}
    }
    return result
}
console.log(newTest(100))
2. 应用闭包
function newTest(num){if(num===0) return true
    if(num===1) return false
    return function(){return newTest(Math.abs(num)-2)
    }
}
console.log(newTest(4)()()) //true
console.log(newTest(6)()()())//true
// 由上可见,须要判断 return 的是不是 function,如果是,须要继续执行,调整一下如下:function reCall(func,arg){var value = func(arg)
    while(typeof value === 'function'){value = value();
    }
    return value 
}

console.log(reCall(newTest,10000)) //true

每次都返回一个匿名函数,再去调用下面的办法,造成一个闭包,匿名函数完后执行相干的参数和局部变量将会开释,不会额定减少堆栈大小,保障每次都是新的。

3. 应用 setTimeout()来解决
function newTest(num) {if(Math.abs(num)-2>=0){setTimeout(function() {newTest(Math.abs(num-2))
        }, 0)
    }else{console.log(num===0?true:false)
    }
};
console.log(newTest(1000000000000))

堆栈溢出之所以会被打消,是因为事件循环操纵了递归,而不是调用堆栈(也就是执行栈)。思路就是不把递归里的函数放到调用栈里,比方通过 setTimeout(宏工作)丢到工作队列中而后依照事件循环来管制, 然而这样的就会有作用域及 this 指针的问题,须要批改一些业务逻辑, 而且调用有一个最小的工夫距离,又是异步的,即时性也不好。
4. 通过 promise 来解决
function newTest(num){if(Math.abs(num)-2>=0){Promise.resolve().then(() => {newTest(Math.abs(num)-2)
        })  
    }else{console.log(num===0?true:false)
    }
}
console.log(newTest(10))

// 应用 promise 把递归放到微工作里执行,原理与 setTimeout 统一,只不过一个是靠宏工作(setTimeout),一个是靠微工作(Promise),通过工夫循环来解决递归里的调用栈问题。// 这个还能承受,微工作即时性挺好, 原理
5. 尾调用优化

ECMAScript 6 标准新增了一项内存治理优化机制,让 JavaScript 引擎在满足条件时能够重用栈帧。尾调用优化的条件就是确定内部栈帧真的没必要存在了

  1. 代码在严格模式下执行
  2. 内部函数的返回值是对尾调用函数的调用
  3. 尾调用函数返回后不须要执行额定的逻辑
  4. 尾调用函数不是援用内部函数作用域中自在变量的闭包
function test(num){if(num===0) return true
    if(num===1) return false
    return test(Math.abs(num-2))
}
console.log(test(1000000000000))

最初

走过路过,不要错过,点赞、珍藏、评论三连~

正文完
 0