关于前端:热点面试题Nodejs-中的垃圾回收机制

44次阅读

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

前言

极度投入,深度沉迷,边界清晰
前端小菜鸡一枚,分享的文章纯属个人见解,若有不正确或可待探讨点可随便评论,与各位同学一起学习~

欢送关注 『前端进阶圈』 公众号,一起摸索学习前端技术 ……
公号回复 加群 扫码 , 即可退出前端交流学习群,长期交流学习 ……
公号回复 加好友,即可添加为好友

文章分为最初总结和步骤解说,可自需查看。废话不少数,先上总结。

  • 总结:

    • Node.js 所运行的内存属于 Chrome V8 所治理的内存,在 Javascirpt 中所创立的对象都是保留在堆内存中,V8 的垃圾回收机制依照对象的存活工夫将内存次要分为:新生代和老生代。

      • 新生代:简略来说,就是存活工夫较短的对象所存储的中央。

        • 次要采纳的是 semispace,它将内存分为了两个空间:From spaceTo space, 例如咱们申明一个新对象,这个新对象会被放入 From space 中,当 From space 快满的时候,会遍历所有的对象,将沉闷对象从 From space copy 到 To space 中。在这个过程中,如果一个对象被 copy 了很屡次,就会被认为是存活工夫较长的对象,将会被放入老生代中。
      • 老生代:相同,就是存活工夫较长的对象所存储的中央。

        • 次要采纳的是 Mark-Sweep (标记革除)算法,它会从执行栈和全局对象上找所有能拜访到的对象,将他们标记为沉闷对象,标记实现之后,进入革除阶段,将没有标记的对象革除(这个过程也就是将革除了对象的内存标记为闲暇状态),最初,将闲暇状态的内存进行开释。
  • 内存机制

    • 整体来讲,Node 的内存分为两局部:Chrome V8 治理的局部 (Javascript 应用的局部) 零碎底层治理的局部(C++/ C 应用的局部)

      • Chrome V8 的内存管理机制

        • Node 程序运行所占用的所有内存称为 常驻内存 , 常驻内存 由以下几局部组成:

          1. 代码区: 寄存行将执行的代码片段
          2. 栈:寄存局部变量
          3. 堆:寄存对象和闭包上下文,V8 应用的垃圾回收机制治理堆内存
          4. 堆外内存:不通过 V8 调配,也不受 V8 治理。是 Buffer 对象的数据存储的中央
        • A:除堆外内存,其余部分均由 V8 治理

          1. 栈的调配与回收十分间接,当程序来到某作用域后,其栈指针下移(也就是回退),整个作用域的局部变量都会出栈,内存将被发出
          2. 最简单的局部是堆的治理,V8 应用垃圾回收机制进行堆的内存治理,也是开发中可能造成内存透露的局部,是开发者的关注点
      • 内存 C /C++ 的局部

        • 这是 Node 的原生局部,也是从根本上区别与前端 js 的局部,包含外围运行库,在一些外围模块的加载过程中,Node 会调用一个名为 js2c 的工具。这个工具会将外围的 js 模块代码以 C 数组的形式存储在内存中,以此来晋升运行效率。
        • 在这个局部,咱们也不会有内存的应用限度,然而作为 C/C++ 扩大来应用大量内存的过程中,危险也是不言而喻的。
        • C/C++ 没有内存回收机制。作为没有 C/C++ 功底的纯前端程序员,不倡议去应用这部分,因为 C/C++ 模块十分弱小,如果对于对象生命周期的了解不够到位,而在应用大内存对象的情境中,很容易就造成内存溢出,导致整个 Node 的解体甚至是零碎的解体。平安的应用大内存的办法就是应用 Buffer 对象。
  • Node 中的 js 引擎也是 chrome 的 V8 引擎,所以垃圾回收机制也属于 V8 中的外部垃圾回收机制。
  • js 中的对象都是保留在堆内存中,在创立过程时,会调配一个默认的堆内存,当对象越来越大时,堆内存会动静的扩充,如果达到最大限度,堆内存就会溢出抛出谬误,而后终止 node.js 过程。
  • V8 的垃圾回收机制依据对象的存活工夫采纳了不同的算法,内存次要分为新生代和老生代。

    • 新生代(存活工夫较短的对象):

      • 新生代内存采取的是将内存分为两个空间(每一部分空间称为 semispace)

        1. From space: 新生命的对象会寄存在此
        2. To space: 当做搬移的空间
      • 新申明的对象会被放入 From space,From space 的对象严密排布,通过指针,上一个对象紧贴着下一个对象,所以内存是间断的,咱们不必放心内存碎片问题。

        • Q:什么是内存碎片?

          • 内存碎片分为:外部碎片和内部碎片两类

            • 外部碎片:零碎为某个性能调配了肯定的内存,然而该性能最终的实现没有用完零碎调配的内存,残余的局部内存就被称为内存碎片中的外部碎片。
            • 内部碎片:有一些连续性内存太小,无奈被零碎调配到某个性能所导致的节约。
      • 当 From space 快满了,就会遍历出沉闷对象,将他们从 From space 复制到 To space, 此时,From space 就空了,而后会将 From 与 To 调换身份。如果一个对象被 copy 了很屡次,就会被认为是存活工夫较长的,将会被移入老生代中。
      • A:这种基于 copy 的算法,长处是能够很好地解决内存碎片的问题,毛病是会节约一些空间作为搬移的空间地位,此外因为拷贝比拟消耗工夫,所以不适宜调配太大的内存空间,更多是做一种辅助垃圾回收。
      • 将存活的对象从一个区复制 (Scavenge 算法:是一种基于 copy 的算法) 到另一个区,对原来的区进行内存开释,重复如此。当一个对象通过屡次复制仍然存活时,这个对象就会被移入老生代当中。
      • Scavenge 算法 (具体实现采纳 Cheney 算法) 原理:在垃圾回收的过程中,就是通过将存活对象在两个 semispace 空间之间进行复制。

        • 长处:工夫短。
        • 毛病:只能应用一半堆内存。新生代对象生命周期短,适宜此算法。
    • 老生代(存活工夫较长的对象):

      • 老生代的空间就比新生代要大得多了,放的是一些存活工夫长的对象,用的是 Mark-Sweep (标记革除)算法。
      • 标记革除的过程:

        1. 从根集 Root Set(执行栈和全局对象)往上找到所有能拜访到的对象,给它们标记为沉闷对象。
        2. 标记完后,就是革除阶段,将没有标记的对象革除,其实就是标记一下这个内存地址为闲暇。
        3. 这种做法会导致 闲暇内存空间碎片化,当咱们创立了一个大的间断对象,就会找不到中央放下。这时候,就要用 Mark-Compact(标记整顿)来将碎片的沉闷对象做一个整合。
        4. Mark-Compact 会将所有沉闷对象拷贝挪动到一端,而后边界的另一边就是一整块的间断可用内存了。
        5. 思考到 Mark-Sweep 和 Mark-Compact 破费的工夫很长,且会阻塞 JavaScript 的线程,所以通常咱们不会一次性做完,而是用 增量标记(Incremental Marking)的形式。也就是做断断续续地标记,小步走的策略,垃圾回收和应用逻辑交替进行。
        6. 另外,V8 还做了并行标记和并行清理,以此来进步执行效率。
      • S: 老生代采取的是标记革除算法,遍历所有对象并标记依然存活的对象,而后再革除阶段将没有标记的对象进行革除,最初将革除后的空间进行开释。

        老生代 新生代(默认) 新生代(最大)
        64 位零碎 1400MB 32MB 64MB
        32 位零碎 700MB 16MB 32MB
    • 注:垃圾回收是影响性能的因素之一,要尽量减少垃圾回收,尤其是全堆垃圾回收

      对象存活工夫 内存空间
      老生代 存活工夫较长或常驻内存的对象 –max-old-space-size 命令设置老生代内存空间的最大值
      新生代 存活工夫较短的对象 –max-new-space-size 命令设置新生代内存空间的大小
  • Q:V8 引擎为什么要将内存分为新老生代呢?

    • R:垃圾回收机制 (GC) 有很多种,但没有一种能胜任所有场景,在理论利用中,须要依据对象的生存周期的长短工夫应用不同的算法,以此来达到最好的成果。在 V8 中,按对象的存活工夫将内存的垃圾回收机制进行不同的分代,而后别离对不同的内存应用不同的高效算法。所以有了新老生代之分。
  • Q: V8 为什么要限度堆内存的大小?

    • R:因为 V8 垃圾回收机制的限度。垃圾回收会引起 js 线程暂停执行;内存太大,垃圾回收工夫太长,在这个思考下,间接限度了堆内存的大小。
  • Q: 如何让内存不受限制?

    • R: 在 Node 中,应用 Buffer 能够读取超过 V8 内存限度的大文件。起因是 Buffer 对象不同于其余对象,它不通过 V8 的内存分配机制。这在于 Node 并不同于浏览器的利用场景。在浏览器中,JavaScript 间接解决字符串即可满足绝大多数的业务需要,而 Node 则须要解决网络流和文件 I / O 流,操作字符串远远不能满足传输的性能需求。
    • R: 在不须要进行字符串操作时,能够不借助 v8,应用 Buffer 操作,这样就不会受到 v8 的内存限度
  • Q:如何查看内存信息?

    • 能够通过 process.memoryUsage() 办法拿到内存相干信息

      process.memoryUsage();
      
      // output:{
          rss: 35454976,
          heapTotal: 7127040,
          heapUsed: 5287088,
          external: 958852,
          arrayBuffers: 11314
      }
      
      /**
       * unit(单位):byte(字节)
          rss:常驻内存大小(resident set size),包含代码片段、堆内存、栈等局部。heapTotal:V8 的堆内存总大小;heapUsed:占用的堆内存;external:V8 之外的的内存大小,指的是 C++ 对象占用的内存,比方 Buffer 数据。arrayBuffers:ArrayBuffer 和 SharedArrayBuffer 相干的内存大小,属于 external 的一部分
      */
  • Q: 如何测试最大内存限度?

    • 写一个脚本,用一个定时器,让一个数组不停地变大,并打印堆内存应用状况,直到内存溢出, 抛出谬误

      const format = function (bytes) {return (bytes / 1024 / 1024).toFixed(2) + "MB";
      };
      const printMemoryUsage = function () {const memoryUsage = process.memoryUsage();
          console.log(`heapTotal: ${format(memoryUsage.heapTotal)}, heapUsed: ${forma(memoryUsage.heapUsed)}`
          );
      };
      const bigArray = [];
      setInterval(function () {bigArray.push(new Array(20 * 1024 * 1024));
          printMemoryUsage();}, 500);
    • A: 不要用 Buffer 做测试。因为 Buffer 是 Node.js 特有的解决二进制的对象,它不是在 V8 中的实现的,是 Node.js 用 C++ 另外实现的,不通过 V8 分配内存,属于堆外内存
    • 测试阐明:应用电脑是 macbook M1 Pro,Node.js 版本为 v16.17.0,应用的 V8 版本是 9.4.146.26-node.22(通过 process.versions.v8 失去)

      // result:
      
      heapTotal: 164.81 MB, heapUsed: 163.93 MB
      
      heapTotal: 325.83 MB, heapUsed: 323.79 MB
      
      heapTotal: 488.59 MB, heapUsed: 483.84 MB
      
      ...
      
      heapTotal: 4036.44 MB, heapUsed: 4003.37 MB
      
      heapTotal: 4196.45 MB, heapUsed: 4163.29 MB
      
      <--- Last few GCs --->
      
      [28033:0x140008000]    17968 ms: Mark-sweep 4003.2 (4036.4) -> 4003.1 (4036.4) MB, 2233.8 / 0.0 ms  (average mu = 0.565, current mu = 0.310) allocation failure scavenge might not succeed
      
      [28033:0x140008000]    19815 ms: Mark-sweep 4163.3 (4196.5) -> 4163.1 (4196.5) MB, 1780.3 / 0.0 ms  (average mu = 0.413, current mu = 0.036) allocation failure scavenge might not succeed
      
      <--- JS stacktrace --->
      FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
      ...
    • 能够看到,是在 4000 MB 之后超出了内存下限,产生堆溢出,而后退出了过程。阐明在我的机器上,默认的最大内存为 4G。
    • 理论最大内存和它运行所在的机器无关,如果你的机器的内存大小为 2G,最大内存将设置为 1.5G。
  • Q: Javascript 的局部是由 ChromeV8 接管的吗?那为什么依然能够应用大量内存创立缓存呢?

    • R: 是的,Chrome 和 Node 都采纳 ChromeV8 作为 JS 的引擎,然而实际上他们所面对的对象是不同的,Node 面对的是数据提供,逻辑和 I /O,而 Chrome 面对的是界面的渲染,数据的出现。因而在 Chrome 上,简直不会遇到大内存的状况,作为为 Chrome 的而生的 V8 引擎天然也不会思考这种状况,因而才会呈现内存限度。而当初,Node 面对这样的状况是不能够承受的,所以 Buffer 对象,是一个非凡的对象,它由更低层的模块创立,存储在 V8 引擎以外的内存空间上。
    • R: 在内存的层面上讲 Buffer 和 V8 是平级的。
  • Q: 如何高效应用内存?

    1. 手动销毁变量

      • js 中能造成作用域的有函数调用、with 和全局作用域
      • 例如,在函数调用时,会创立对应的作用域,在执行完结后销毁,并且在该作用域申明的局部变量也会被销毁

        1. 标识符查找(即变量名)先查找以后作用域,再向下级作用域,始终到全局作用域
        2. 变量被动开释 全局变量要直到过程退出才开释,导致援用对象常驻老生代,能够用 delete 删除或者赋 undefined、null(delete 删除对象的属性可能烦扰 v8, 所以赋值更好)
    2. 慎用闭包

      • 闭包是内部作用域拜访外部作用域的办法,得益于高阶函数个性

        var foo = function() {var bar = function() {
        var local = "外部变量";
        return function() {return local;};
        };
        var baz = bar();
        console.log(baz());
        };
        
        // 从下面代码知 bar()返回一个匿名函数, 一旦 有变量援用它,它的作用域将不会开释,直到没有援用。// 注:把闭包赋值给一个不可控的对象时,会导致内存透露。应用完,将变量赋其余值或置空
    3. 大内存应用

      1. 应用 stream,当咱们须要操作大文件,应该利用 Node 提供的 stream 以及其管道办法,避免一次性读入过多数据,占用堆空间,增大堆内存压力。
      2. 应用 Buffer,Buffer 是操作二进制数据的对象,不论是字符串还是图片,底层都是二进制数据,因而 Buffer 能够实用于任何类型的文件操作。
        Buffer 对象自身属于一般对象,保留在堆,由 V8 治理,然而其贮存的数据,则是保留在堆外内存,是有 C ++ 申请调配的,因而不受 V8 治理,也不须要被 V8 垃圾回收,肯定水平上节俭了 V8 资源,也不用在意堆内存限度。
  • Q: 内存泄露?

    • 起因:缓存,队列耗费不及时,作用域未开释等
    • 缓存:

      • 限度内存当缓存,要限度好大小,做好开释
      • 过程之间不能共享内存,所以用内存做缓存也是
    • 为了减速模块引入,模块会在编译后缓存,因为通过 exports 导出(闭包), 作用域不会开释,常驻老生代。要留神内存透露。

      var arr = [];
      exports.hello = function() {arr.push("hello" + Math.random());
      };
      // 局部变量 arr 不停减少内存占用,且不会开释,如果必须如此设计,要提供开释接口
    • 队列状态

      • 在生产者和消费者两头
      • 监控队列的长度,超过长度就回绝
      • 任意的异步调用应该蕴含超时机制
    • 内存泄露排查的工具

      • node-heapdump

        1. 装置 npm install heapdump
        2. 引入 var heapdump = require(‘heapdump’);
        3. 发送命令 kill -USR2,heapdump 会抓拍一份堆内存快照,文件为 heapdump-..heapsnapshot 格局,是 json 文件
      • node-memwatch

        var memwatch = require('memwatch');
        memwatch.on('leak', function(info) {console.log('leak:');
            console.log(info);
        });
        memwatch.on('stats', function(stats) {console.log('stats:') console.log(stats);
        });
      • 在过程应用 node-memwatch 后,每次全堆垃圾回收,会触发 stats 事件,该事件会传递内存的统计信息

        stats: {
            num_full_gc: 4, //   第几次全堆垃圾回收
            num_inc_gc: 23, //    第几次增量垃圾回收
            heap_compactions: 4, //  第几次对老生代整顿
            usage_trend: 0, // 应用趋势
            estimated_base: 7152944, // 预估基数
            current_base: 7152944, // 以后基数
            min: 6720776, //  最小
            max: 7152944  // 最大
        }
    • 如果通过间断的 5 次垃圾回收后,内存仍没有被开释,象征有内存透露,node-memwatch 会触发 leak 事件。

文章特殊字符形容:

  • 问题标注 Q:(question)
  • 答案标注 R:(result)
  • 注意事项规范:A:(attention matters)
  • 详情形容标注:D:(detail info)
  • 总结标注:S:(summary)

最初:

  • 欢送关注 『前端进阶圈』 公众号,一起摸索学习前端技术 ……
  • 公号回复 加群 扫码, 即可退出前端交流学习群,长期交流学习 ……
  • 公号回复 加好友,即可添加为好友

正文完
 0