• nodejs中的垃圾回收


    前言

      由于 ndoejs 是基于 v8 引擎的,而且对于内存的使用也是 v8 申请和分配的。所以这里的垃圾回收实际就是 v8 下的垃圾回收机制。

    V8的内存限制

      在默认情况下,nodejs 只能使用物理内存的部分内存,具体大小为 1.4G(64位系统) 和 0.7G(32位系统),无法操作大文件(比如一个2G大小的文件),使用内存超过限制就会进程退出。

    • 原因

      • v8 最初是为浏览器而设计的,很少会遇到使用大内存的场景,而 nodejs 恰恰就是基于 v8 构建的,所以 nodejs 也受到了此限制;
      • 垃圾回收时会引起应用逻辑暂停执行,垃圾越多,暂停的时间越长,而整体应用的等待时间就越长,这样直接影响应用的体验,所以直接限制堆内存的使用。
    • 解除限制

        node在启动时可以添加 --max-old-space-size 或 --max-new-space-size 来调整限制的大小,如:

          node --max-old-space-size=2000 index.js // 单位为MB

          node --max-new-space-size=500 index.js // 单位为MB

        当然,就算显示设定内存的限制,node 一旦启动,内存限制是无法动态改变的。

    V8的垃圾回收算法

      v8的垃圾回收策略主要基于分代式的垃圾回收机制。将对象在内存中的存活时间进行分代,然后再在不同的分代中进行不同的回收算法。

    • v8的内存分代

        在v8中,主要将内存分为新生代和老生代两代,所以v8的堆内存大小为 新生代内存占用的大小加上老生代内存占用的大小。

        新生代:主要为存活时间较短的对象;

        老生代:主要为存活时间较长或常驻内存的对象。

        

         --max-old-space-size 就是设置老生代的内存大小,--max-new-space-size 就是设置新生代的内存大小。

        但是在 node 执行过程中,如果内存的分配超过限制值,就会造成进程错误。

    • 新生代回收算法

         在新生代中,垃圾回收算法采用的是 Scavenge 的具体实现的 Cheney 算法。

        Cheney 算法将堆内存一分为二,分别为 From 和 To;

        

        在进行内存分配时,是在 From 中进行分配的;

        而在垃圾回收时,会检查 From 中的存活对象,并将这些存活对象复制到 To 中;

        然后再将非存活对象进行释放;

        在下一轮回收时,将 To 中的存活对象复制到 From 中,而此时,To 就变成了 From,From 变成了 To。

        每次垃圾回收,From 和 To 会互换。

        这个算法在复制时只复制了存活对象,所以在垃圾回收时较快,但是只利用了堆内存的一半,这是典型的空间换时间算法。由于在新生代中,对象的存活时间较短,所占用的内存空间也较少,所以就非常适合这个算法。

      • 晋升

           在复制的过程中,如果一个对象经过多次复制并依然存活,那么它会被当成生命周期较长的对象,并把它移动到老生代中,这种从新生代移动到老生代中的过程就叫晋升。

          晋升一般需要满足两个条件:

            1:一个对象是否已经经历过回收了,如果经历过了就复制到老生代中,没有就复制到 To 中;

            2:判断 To 空间的已使用内存的占比,如果在复制过程中,To 空间的已使用内存占比超过了 25%,就将此对象复制到老生代中。

              25% 是因为,当 To 转换为 From 时,内存分配需要在 From 中分配,而如果占比过高,那么就会影响后续的内存分配。

    • 老生代回收算法

        老生代中使用的是其他回收算法,如果在老生代中继续使用 Scavenge ,那么复制的效率会很低,因为在老生代中的对象是存活时间较长的,存活的对象也较多,其次就是会浪费一半的内存空间。

        所以在老生代中是使用 Mark-Sweep 和 Mark-Compact 两种算法相结合的方式。

      • Mark-Sweep

          Mark-Sweep 是标记清除的意思,回收分两个阶段:标记和清除,在标记的时候遍历内存中的所有存活对象,然后清除没有标记的对象。因为在老生代中,对象存活时间较长,说明存活对象较多,非存活对象较少,所以使用 Mark-Sweep 就非常高效。

          

           黑色部分代表非存活对象。

          Mark-Sweep 的缺点是清除掉非存活对象后,内存状态不是连续的,而此时如果需要分配一个大对象,所有的内存碎片都无法满足,这样就会提前触发回收。

          

      • Mark-Compact   

          Mark-Compact 就是解决 Mark-Sweep 回收后的问题的,意为标记整理。使用 Mark-Sweep 回收后,再使用 Mark-Compact 将所有存活对象往一端移动,再清理掉边界外的内存。

          

        在 v8 中,老生代中主要还是用 Mark-Sweep ,在内存不足以分配时才使用 Mark-Compact。

      • 增量回收

          由于垃圾回收时会造成应用逻辑停顿(暂停执行),而在老生代空间中一般存活对象较多,需要标记大量对象,造成停顿较长,影响应用。所以在标记时进行增量标记。

          将一次性需要标记的对象分多次进行,标记一些,再让应用执行,然后再暂停标记一些,再让应用执行.....,循环交替,达到改善的目的。

          同样在清理与整理时也有增量操作。

    常见内存泄露

      一般造成内存泄露的原因主要是:缓存、作用域未释放、队列消费不及时。

    • 缓存

        一般缓存造成的泄露主要就是把内存当缓存使用,一旦把内存当缓存使用,那么这块内存中的对象就会常驻在老生代中,随着缓存增大,超过老生代的空间,进而造成泄露。

        限制缓存大小,使用其他缓存(redis)代替都能有效解决。

    • 作用域未释放

        比较经典的案例就是在导入的模块,而模块内的某一方法为其私有变量添加了内存占用。如下:

    1 var leakArray = [];  
    2 
    3 exports.leak = function () { 
    4 
    5   leakArray.push("leak" + Math.random()); 
    6 
    7 };

        每执行一次 leak ,其 leakArray 就会添加一次内存占用。这是因为导入模块,会将模块缓存起来,而缓存的模块是常驻在内存中的,直到进程退出。    

    • 队列消费不及时

        如收集日志,将日志存到数据库中,通常日志非常多,而写入数据库的效率又比较慢,而此时就会有很多的数据库写入操作,而正因为这些操作的堆积,操作的作用域也没被释放,最终造成泄露。

    Welcome to my blog!
  • 相关阅读:
    build.gradle中引入jar
    SparkSQL 运行异常:org/codehaus/janino/InternalCompilerException 执行SparkSQL时出现异常:
    MapReduce wordcount 输入路径为目录 java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$POSIX.stat(Ljava/lang/String;)Lorg/apache/hadoop/io/nativeio/NativeIO$POSIX$Stat;
    spark远程调试代码报错 StandaloneAppClient$ClientEndpoint: Failed to connect to master 192.168.126.128:7077
    springboot集成spark
    springboot集成spark并使用sparksql
    SparkSession、SparkContext、SQLContext和HiveContext之间的区别。
    启动thriftserver.sh
    Hive学习系列:maven+springboot+CDH环境下,连接Hive进行操作
    【网络流】——P2756 飞行员配对方案问题
  • 原文地址:https://www.cnblogs.com/blogCblog/p/14584745.html
Copyright © 2020-2023  润新知