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


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

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

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

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

    2.JS 的内存回收 

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

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

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

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

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

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

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

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

    例子:

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

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

    注意点:

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

    1. <script>
    2. function fn() {
    3. let arr = new Array(100000)
    4. return function () {
    5. console.log(arr);//这里产生闭包,导致fn函数的局部变量arr无法被回收,一直存在于内存中
    6. }
    7. }
    8. let fun = fn();
    9. let test = fun();
    10. // fun = null;
    11. script>

     

     

     

    修改代码进行优化:

     

     

    三、内存溢出

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

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

    四、常见的js内存泄露

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

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

    1. function test() {
    2. names = 'hsq'
    3. }
    4. test();
    5. console.log(names);//hsq

     相当于

    1. function test() {
    2. window.names = 'hsq'
    3. }
    4. test();
    5. console.log(window.names);//hsq

    解决办法:

    ①使用js严格模式

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

    1. //js文件
    2. "use strict";
    3. function test() {
    4. names = 'hsq'
    5. }
    6. test();
    7. console.log(names);//报错

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

    1. function test() {
    2. names = 'hsq'
    3. }
    4. test();
    5. console.log(names);//hsq
    6. names = null
    7. console.log(names);//null

    2、计时器和回调函数

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

    案例:

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

    图解:

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

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

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

    3、DOM引用

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

    案例:

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

    ps:

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

    解决方案:

    利用null释放内存

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

    注意点:

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

     
    4、闭包

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

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

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

    1. <script>
    2. //fn这个函数作用域访问了另一个函数fn里面的局部变量num,那么函数fn就是一个闭包函数
    3. function fn() {
    4. let num = 3
    5. return function () {
    6. //console.log(num)语句依赖fn函数的局部num变量,所以会产生闭包,导致num变量不能被内存回收,
    7. // 因为这里的函数是返回函数,不知道什么时候会再次调用,
    8. //所以内存需要一直保存num变量,所以不会进行回收
    9. console.log(num);
    10. }
    11. }
    12. let test = fn();//此时fn函数已经执行,正常的js垃圾回收机制会把fn函数的局部变量num进行回收,但是由于闭包的产生,所以此时不会进行回收处理
    13. test()//3,返回函数依然可以访问fn函数的局部变量num
    14. window.setTimeout(() => {
    15. test()//3,经过3秒后执行返回函数,依然可以访问fn函数的局部变量num
    16. }, 1000)
    17. script>

    ps:

    关键在于,闭包之间是共享作用域的, //console.log(num)语句依赖fn函数的局部num变量,所以会产生闭包,导致num变量不能被内存回收,而且每次调fn函数时,都会产生一个返回函数的引用,引用没有清除掉,也会造成内存浪费。

    解决方案:

    参考文章:

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

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

    1. <script>
    2. //fn这个函数作用域访问了另一个函数fn里面的局部变量num,那么函数fn就是一个闭包函数
    3. function fn() {
    4. let num = 3
    5. return function () {
    6. //console.log(num)语句依赖fn函数的局部num变量,所以会产生闭包,导致num变量不能被内存回收,
    7. // 因为这里的函数是返回函数,不知道什么时候会再次调用,
    8. //所以内存需要一直保存num变量,所以不会进行回收
    9. console.log(num);
    10. }
    11. }
    12. let test = fn();//此时fn函数已经执行,正常的js垃圾回收机制会把fn函数的局部变量num进行回收,但是由于闭包的产生,所以此时不会进行回收处理
    13. test()//3,返回函数依然可以访问fn函数的局部变量num
    14. test = null;//返回函数运行后,将返回函数的引用(test变量)清除掉,返回函数的引用被清除掉后,返回函数就会被销毁掉,同时取消对fn函数的局部变量num的依赖,解决闭包产生的内存浪费
    15. window.setTimeout(() => {
    16. let test = fn();//此时fn函数已经执行,正常的js垃圾回收机制会把fn函数的局部变量num进行回收,但是由于闭包的产生,所以此时不会进行回收处理
    17. test()//3,经过3秒后返回函数依然可以访问fn函数的局部变量num
    18. test = null;//返回函数运行后,将返回函数的引用(test变量)清除掉,返回函数的引用被清除掉后,返回函数就会被销毁掉,同时取消对fn函数的局部变量num的依赖,解决闭包产生的内存浪费
    19. }, 1000)
    20. script>

    五、如何避免内存泄漏

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

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

    参考文章:

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

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

  • 相关阅读:
    多线程面试题(三)
    代码随想录第45天|70. 爬楼梯,322. 零钱兑换,279.完全平方数
    spire.pdf盖章(无水印免费无限制)
    verilog移位寄存器实现序列检测
    Web1.0、Web2.0 和 Web3.0 的区别
    猿创征文|Vue源码分析(响应式)
    前后端分离--前置路由守卫(登录过滤)和整合shiro安全框架
    【go学习笔记】Go errors 最佳实践
    数字验证学习笔记——UVM学习2 覆盖方法
    25 VueComponent 的生命周期
  • 原文地址:https://blog.csdn.net/h18377528386/article/details/126373476