• < 今日份知识点:谈谈内存泄漏 及 在 Javascript 中 针对内存泄漏的垃圾回收机制 >



    💬 前言

    说起 “ 内存泄漏 ”,科班出身的卷王们应该第一时间会想到 C语言的指针,对内存的分配 或者 其他操作。程序需要运行,必然会占用内存,就好比我们在电脑上运行程序,就必须向运行的软件程序提供内存,它才能运行。

    程序运行会生成对应的服务进程,对于一些持续的服务进程,它会持续的占用内存,但是当服务运行一个来回时,如果上次运行申请的内存,没有得到及时的释放,就会导致 “ 内存泄漏 ”通俗点说,就是指被遗漏的内存持续占用堆积

    这样的结果显而易见,会造成服务性能降低,严重会导致服务卡顿等现象。所以这里就需要我们去了解 “ 内存泄漏 ” 的原理,避免出现内存持续占用的情况! 接下来,就由小温带大家去了解 “ 内存泄漏 ” 相关的知识点吧!


    👉 一、“ 内存泄漏 ”简述

    内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存

    并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费

    正如前言所述,程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存

    对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃

    在这里插入图片描述
    C语言 为例,由于 C语言 是 手动管理内存,如果程序设计不当,是非常容易导致 “ 内存泄漏 ”

    // 申请内存
    char * buffer;
    buffer = (char*) malloc(42);
    
    // Do something with buffer
    ...
    
    // 使用free方法释放内存
    free(buffer);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面案例中,在C语言中,使用malloc方法用来申请内存,使用完毕之后,必须自己用 free 方法释放内存。

    这种手动管理内存的方式非常麻烦,在申请内存的变量多时,操作累赘重复。所以为了提高编写效率,现在大多数语言提供自动内存管理,减轻程序员的负担,这被称为 " 垃圾回收机制 "

    👉 二、 垃圾回收机制

    Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存,自动进行释放等操作!

    > 原理

    垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

    那么,可能会有卷王疑惑了,垃圾收集器怎么知道我哪些变量还要使用,哪些变量在指定位置不需要使用了呢? 这就引出了 “ 垃圾回收机制 ” 的实现方式了,内容如下:

    通常情况下有两种实现方式:

    • 标记清除 : 通过判断变量进出执行环境,标记变量在指定位置所处的状态,若变量已离开执行所处的环境,此变量将等待 “ 垃圾回收

    • 引用计数 : 通过判断变量被引用的次数,判断此变量是否任需使用。如果引用次数为 “ 0 ”,那么此变量占用的内存将会被回收!

    > 实现方式(详)

    ① 标记清除

    判断原理 : 当变量进入执行环境时,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能被释放,当变量离开环境时,则将其标记为“离开环境

    垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。

    垃圾回收机制 - 标记清除

    在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了

    随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

    优点:

    • 实现简单,标记情况无非是打与不打的两种情况,通过二进制(0和1)就可以为其标记。
    • 能够回收循环引用的对象
    • v8引擎使用最多的算法。

    缺点:

    在清除垃圾之后,剩余对象的内存位置是不变的,就会导致空闲内存空间不连续。这样就出现了内存碎片,并且由于剩余空间不是整块,就需要考虑内存分配的问题。

    举个例子:

    var m = 0,n = 19  		// 把 m,n,add() 标记为进入环境。
    add(m, n) 				// 把 a, b, c标记为进入环境,当此方法体运行完,代表内部的局部变量离开环境。
    console.log(n) 			// a,b,c标记为离开环境,等待垃圾回收。
    
    function add(a, b) {
      a++
      let c = a + b
      return c
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ② 引用计数

    语言引擎有一张"引用表",保存了内存里面所有变量引用的内存资源(通常是各种值)的引用次数。如果一个值的引用次数是 0 ,就表示这个值不再用到了,因此可以将这块内存释放。

    tips: 如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。

    const arr = [1, 2, 3, 4];  // arr 占用的内存泄漏
    console.log('hello world');
    
    • 1
    • 2

    上面代码中,数组arr, 存储的 [1, 2, 3, 4] 是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存。

    如果需要这块内存被垃圾回收机制释放,只需要设置如下:

    arr = null
    
    • 1

    通过设置arr为null,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就会立刻被垃圾回收了。

    优点:

    • 引用计数为零时,发现垃圾立即回收
    • 最大限度减少程序暂停

    缺点:

    • 无法回收循环引用的对象
    • 空间开销比较大

    👉 三、常见的几种内存泄漏的情景

    ①在某个局部代码块中,出现全局变量操作

    function foo(arg) {
        bar = "this is a hidden global variable";
    }
    
    • 1
    • 2
    • 3

    ② 全局变量可能由 this 创建

    function foo() {
        this.variable = "potential accidental global";
    }
    // foo 调用自己,this 指向了全局对象(window)
    foo();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    tips:上述使用严格模式,可以避免意外的全局变量

    ③ 定时器也常会造成内存泄露

    var someResource = getData();
    setInterval(() => {
        var node = document.getElementById('Node');
        if(node) {
            // 处理 node 和 someResource
            node.innerHTML = JSON.stringify(someResource));
        }
    }, 1000);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放

    ④ 包括之前所说的闭包,维持函数内局部变量,不能及时释放,一样会造成内存泄漏

    function bindEvent() {
      var obj = document.createElement('XXX');
      var unused = function () {
        console.log(obj, '闭包内引用obj obj不会被释放');
      };
      obj = null; // 解决方法
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ⑤ 没有清理对DOM元素的引用同样造成内存泄露

    const refA = document.getElementById('refA');
    document.body.removeChild(refA); // dom删除了
    console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
    refA = null;
    console.log(refA, 'refA'); // 解除引用
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ⑥ 使用事件监听addEventListener监听的时候,在不监听的情况下使用removeEventListener取消对事件监听

    以上为日常开发中,比较常见的内存泄漏场景,需要在平时开发中加以留意!

    > 小结

    虽然JavaScript提供了一套 垃圾回收机制 , 但是并不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要及时检查是否还存在对它们的引用。如果是的话,就必须手动解除引用,这是日常开发中,需要注意的JavaScript书写规范!

    今天的内容就此结束,如果觉得对你有帮助的话,不妨点点赞,支持一下小温呀!


    📃 参考文献

    往期内容 💨

    🔥 < 今日份知识点:浅述对 “ Vue 插槽 (slot) ” 的理解 以及 插槽的应用场景 >

    🔥 <恢复更新进度ing:今天浅聊一下前端CSS样式 及 书写的规范 >

    🔥 < 每日份知识快餐:axios是什么?如何在Vue中 封装 axios ? >

    🔥 < 今日份知识点:web常见的攻击方式(网络攻击)有哪些?如何预防?如何防御呢 ? >

  • 相关阅读:
    javaweb基础:tomcat的安装,以及目录结构
    HTML之背景颜色、图片、超链接
    自增主键用完了该怎么办
    【计算机基础知识8】深入理解OSI七层模型
    Word使用小技巧
    Java基础方法重写
    从github上下载下来的代码下载依赖提示: An unknown git error occurred
    基于SSM框架的图片分享及评价网站设计与实现 毕业设计-附源码201524
    使用C++11实现对象池
    【蓝桥2025备赛】容斥原理
  • 原文地址:https://blog.csdn.net/MrWen2395772383/article/details/125486988