美文网首页
JS 的垃圾回收机制

JS 的垃圾回收机制

作者: limengzhe | 来源:发表于2023-08-14 22:00 被阅读0次

    前言

    垃圾回收(Garbage Collection)是一种内存管理机制,用于检测和清理不再被程序使用的内存。垃圾回收器会在 JS 引擎(浏览器或者 nodejs)内部周期性地运行,开发者无需手动操作。

    但是,了解垃圾回收机制的工作原理有助于我们写出更加高效的 JS 代码,使 JS 引擎更好的帮助我们完成垃圾回收,避免我们开发的应用出现内存泄漏问题。

    垃圾是怎样产生的?

    JS 中的数据类型有原始类型和引用类型,原始类型占用的内存极小,一般是字符串、数字、布尔值这些,他们被存放在栈(stack)中。引用类型可以是数组、普通对象或者函数,他们一般会包含较多的数据,所以引用类型的实际数据存放在内存的堆(heap)中,然后在栈中会存放一个指向该实际数据的地址。

    const str = "abc" // 原始类型
    const obj = { foo: "bar" } // 引用类型
    

    在 JS 中每声明一个变量,该应用所占的内存就会相应的增加,但机器的内存是有限的,内存占用不能无限制的增加。那些不再被程序使用的数据,就被称为垃圾,他们所占用的内存就会被 JS 引擎的垃圾回收机制回收。

    垃圾回收机制会回收哪些垃圾?

    当一个对象不在任何地方被引用时(如何判断是否被引用由算法决定,后面会介绍算法),垃圾回收器就需要将这些对象进行标记,并在适当的时机回收它们的内存。

    以下面这段代码为例:

    function myFunction() {
      const obj = { foo: "bar" }
      // do something
    }
    
    myFunction()
    

    这段代码中的 obj 对象在 myFunction 函数执行之后,就已经没有被引用了,就需要被回收。

    但当某个对象从开发角度上来说不再被使用了,却意外的仍然在某个地方被引用,垃圾回收器就无法回收它的内存,就会造成内存泄漏(内存逐渐累积,程序占用的内存越来越多,当超过系统的可用内存时,就会造成程序崩溃)。

    比如下面这段代码中 obj 对象就有可能不会被回收(取决于算法):

    function myFunction() {
      const obj = { foo: "bar" }
    
      setTimeout(() => {
        console.log(obj.foo)
      }, 1000)
    }
    
    myFunction()
    

    因为 obj 作为闭包中的引用传递给了定时器的回调函数,即使 myFunction 执行完毕,由于定时器没有被清除,obj 仍然被定时器回调函数持有引用,就可能导致 obj 不会被垃圾回收。

    垃圾回收的算法

    目前 JavaScript 中的垃圾回收机制主要基于以下两种算法:

    引用计数(Reference-Counting)算法

    该算法的原理是,记录每个对象的引用次数,引用增加时计数器加一,引用减少时减一,当引用计数变为零时,就表示没有任何引用指向该对象了,该对象就可以被回收。

    这种规则导致该算法有一个缺点:无法回收循环引用的对象,因为互相引用的对象在程序结束时都至少有一次引用。比如下面这段代码:

    function myFunction() {
      const a = {}
      const b = {}
      a.b = b // a 引用了 b
      b.a = a // b 引用了 a
    }
    
    myFunction()
    

    在该算法下,即使在 myFunction 执行之后,对象 ab 也不会被回收。

    IE 6 和 IE 7 使用的就是这种算法。

    标记-清除(Mark-and-Sweep)算法

    这是一种更常见的垃圾回收算法。核心概念是对象是否可达,当一个对象不在任何地方被引用时,它就是不可达,相反就是可达。该算法会从根对象(全局对象、函数执行上下文)开始,通过遍历对象之间的引用关系,记录所有可达和不可达的对象。然后,回收器清除不可达的对象,释放其内存。

    该算法解决了引用计数算法的循环引用问题,在刚刚那段代码中,函数 myFunction 执行之后,对象 ab 从全局对象出发就无法获取了。因此,他们将会被回收。

    从 2012 年起,所有现代浏览器都使用了这种算法。

    Chrome 和 nodejs 的垃圾回收算法

    Chrome 和 nodejs 都采用了谷-歌开源的 V8 引擎。V8 引擎的垃圾回收机制采用了标记-清除算法,但在此基础做了一些优化。

    V8 引擎将内存分为新生代(Young Generation)和老生代(Old Generation)。大多数对象在新生代中创建,经过一定时间后,如果它们仍然存活,就会被晋升到老生代。

    Scavenger 垃圾回收(新生代)

    新生代使用了 Scavenger 垃圾回收算法,它将内存划分为一个存活区域和一个空闲区域。对象首先被分配到存活区域,当存活区域满时,会执行垃圾回收操作,将存活的对象复制到空闲区域,并清空存活区域。

    Mark-Sweep-Compact 垃圾回收(老生代)

    老生代中使用了 Mark-Sweep-Compact(标记-清除-整理)垃圾回收算法。它首先标记所有的存活对象,然后清除掉未被标记的对象,最后进行内存整理,使存活对象连续排列,减少内存碎片。

    增量垃圾回收

    V8 引擎还支持增量垃圾回收。他会将垃圾回收操作分成多个小步骤执行,每个步骤之间会插入一些 JavaScript 代码的执行,从而避免长时间的垃圾回收造成的界面卡顿。

    空闲时间垃圾回收

    V8 引擎还在空闲时间执行部分垃圾回收操作,以充分利用闲置的计算资源。这些时间段可能是在程序等待用户输入、网络请求返回、或者其他暂时没有任务需要处理的情况下出现的。

    需要手动清除的内存

    垃圾回收机制会根据算法智能的回收大部分的内存,但由于业务逻辑的关系,它无法明确知道在我们的写的(垃圾)代码中,哪些对象其实是不再使用的,所以我们在开发过程中需要及时的清除不需要的事件监听、定时器、计时器,避免循环引用,以及避免使用闭包。

    清除事件监听

    const myButton = document.getElementById("myButton")
    
    function handleClick() {
      console.log("Button clicked!")
    }
    
    // 添加事件监听器
    myButton.addEventListener("click", handleClick)
    
    // 在页面卸载或元素移除时解除事件监听器
    window.addEventListener("beforeunload", () => {
      myButton.removeEventListener("click", handleClick)
    })
    

    执清除定时器、计时器

    const timer = setTimeout(() => {}, 500)
    
    // 在页面卸载或元素移除时解除事件监听器
    window.addEventListener("beforeunload", () => {
      clearTimeout(timer)
    })
    

    手动调用垃圾回收

    一般情况下我们无需手动调用垃圾回收,但有些浏览器支持主动触发垃圾回收。

    IE 浏览器

    if (typeof window.CollectGarbage === "function") {
      window.CollectGarbage()
    }
    

    Opera 浏览器

    if (window.opera && typeof window.opera.collect === "function") {
      window.opera.collect()
    }
    

    总结

    总而言之,垃圾回收是一项必不可少的内存管理机制,希望大家都能理解其原理,并运用在实际开发中,避免造成内存泄漏的问题,提高应用程序的性能和用户体验。


    参考文档

    相关文章

      网友评论

          本文标题:JS 的垃圾回收机制

          本文链接:https://www.haomeiwen.com/subject/wgucmdtx.html