史上最全GC原理

史上最全GC原理

什么是垃圾

定义

  • 释放已占用的内存,防止内存泄露
  • 清除已经死亡或者长时间未使用的对象内存

语言特性

  • c++手动回收垃圾

    • 忘记回收
    • 回收多次
  • java 自动回收

如何定位垃圾

引用计数法

  • 对象头中分配一片空间用于存储对象引用次数
  • 程序执行过程中完成,非STW
  • 注意:Recycler 算法可解决循环引用,但在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法

根可达性分析算法

  • GC Root

    • 虚拟机栈中引用的对象

      public static void testGC(){
      StackLocalParameter s = new StackLocalParameter(“localParameter”);
      s = null;
      }


    • 方法区中类静态属性引用的变量

    • 方法区中常量引用的对象

    • 本地方法栈JNI中引用的对象

      任何 native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

  • 通过GC roots对象作为起点开始向下搜索引用的对象,找到的对象都为存活对象即可达,其他没有标记的对象都为垃圾

怎么回收垃圾

有哪些回收方法

  • 标记清除 mark sweep

    • 1、从GC Root遍历对象图,标记出垃圾对象;2、再次遍历清除
    • 产生碎片,内存不连续,效率偏低(两遍扫描)
  • 复制copying

    • 1、内存分为两块;2、第一块使用完成后将存活的对象复制到第二块;3、清除第一块内存;
    • 没有碎片,效率高,但浪费空间,大对象时复制成本较高
  • 标记整理 mark compact

    • 1、标记出所有存活对象;2、对存活对象按照整理顺序(Compaction Order)整理到内存的一端;3、清理端以外的内存
    • 没有碎片、无浪费空间,但效率偏低(两遍扫描,引用指针需要调整,内存变动频繁)
  • 分代算法Generational Collection

    • java堆空间

      • 新生代1/3

        • Eden区8/10

          • 98%的对象朝生夕死
        • From区1/10

        • To区1/10

      • 老年代2/3

        • 哪些对象会进入

          • 大对象

            • 需要大量连续内存空间的对象,避免在新生代产生大量复制
          • 长期存活对象

            • 对象头中存放对象年龄,每经过一次minorgc年龄增加一次,默认到15时会进入

              • 可配置:MaxTenuringThreshold
          • 动态对象年龄

            • 年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升

              • 有点负载均衡感觉
        • 常用算法

    • 是以上三种回收算法的组合算法

jvm中有哪些收集器

  • Serial old

  • 分代收集器

    • ParNew

      • 采用复制算法的多线程收集器
      • 主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。
    • CMS

      • 目标:获取最短回收停顿时间

      • 算法:三色标记+标记清除算法+增量更新算法

      • 步骤

        • 1、初始标记:STW,标记GC Root直接引用的对象;
          2、并发标记:从GC Root引用对象开始遍历对象图,标记出可达对象;;
          3、重新标记:STW,采用增量更新算法重新标记2步因用户线程增加引用的对象;
          4、并发清理:清理未标记的垃圾对象;
          5、并发重置:重置本次GC过程中的标记数据;



      • 问题

        • 并发

          • 抢占用户线程cpu资源

            • CMS默认回收线程数是(CPU个数+3)/4

              这个公式的意思是当CPU大于4个时,保证回收线程占用至少25%的CPU资源,这样用户线程占用75%的CPU,这是可以接受的。

              但是,如果CPU资源很少,比如只有两个的时候怎么办?按照上面的公式,CMS会启动1个GC线程。相当于GC线程占用了50%的CPU资源,这就可能导致用户程序的执行速度忽然降低了50%,50%已经是很明显的降低了。

            • 解决办法:incremental mode(增量模式),执行过程中GC线程和用户线程交替执行

          • 浮动垃圾

            • 并发清理过程中产生浮动垃圾,可以忽略,下次清理

            • 解决办法

              • 提前回收机制:CMSInitiatingOccupancyFraction参数默认是内存占用92%时启动GC

                • 如果设置99%,这是需要内存分配1%时会Concurrent Mode Failure错误,这是CMS默认启动Serial Old收集器,效率更慢
              • 动态检查机制:UseCMSInitiatingOccupancyOnlyCMS参数设置CMS会根据历史记录,预测老年代还需要多久填满及进行一次回收所需要的时间。在老年代空间用完之前,CMS可以根据自己的预测自动执行垃圾回收。

          • GC执行过程不确定

            • 在并发标记或者清理阶段会出现还没有回收完成又一次触发fullgc,这时会出现concurrent mode failure错误,此时会STW,用户serioal old处理
        • 标记清除算法

          • 产生碎片化内存,分配效率慢

            • 解决办法

              • UseCMSCompactAtFullCollection参数(默认开启),在Full GC后开启内存碎片整理,但是STW
              • XX:CMSFullGCsBeforeCompaction,参数表示经历多少次fullgc后对内存空间压缩整理,默认为0,每次fullgc会压缩
      • 最佳实践配置

        • -XX:+UseConcMarkSweepGC
          -XX:CMSInitiatingOccupancyFraction=80 //回收内存占比
          -XX:+UseCMSInitiatingOccupancyOnly //启动动态检查机制
          -XX:CMSFullGCsBeforeCompaction=5//设置经历多少次fc后开始压缩整理碎片


      • 应用场景

        • 多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除
  • 分区收集器

    • G1

      G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。==这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率

      • 目标:针对大内存、达到实时高效、高吞吐量

      • 算法:三色标记+复制+标记压缩+STAB

      • 基本概念

        • 分区region

          • 物理分区,逻辑分代,内存区域分为E O H S等,每个分代内存可以不连续

            E代表是Eden区,S代表Survivor,O代表Old区,H代表humongous表示巨型对象(大小大小Region空间一半的对象)

          • 单个分区取值1-32M,必须是2的幂次,-XX:G1HeapRegionSize

          • 最多有2048个分区

        • SATB

          • Snapshot-At-The-Beginning,GC初始标记阶段对堆内存活的对象做一次快照,作用是维持并发GC的正确性

          • 如何保证正确性

            • Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS(上一次标记的位置)和nextTAMS(下次标记的位置)。在TAMS以上的对象是新分配的,这是一种隐式的标记

              • 解决了并发期间新对象分配
            • 对象的引用被替换时,通过write barrier对引用字段复制进行环切AOP, 将旧引用记录下来,所以效率会低些,然后在最终标记阶段只扫描出有write barrier记录的对象

              • 解决了灰色对象到白色对象的引用断开
          • 问题:如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage,STAB精度偏低

        • 写屏障

          这块涉及到SATB标记算法的原理,SATB是指start at the beginning,即在并发收集周期的第一个阶段(初始标记)是STW的,会给所有的分区做个快照,后面的扫描都是按照这个快照进行;在并发标记周期的第二个阶段,并发标记,这是收集线程和应用线程同时进行的,这时候应用线程就可能修改了某些引用的值,导致上面那个快照不是完整的,因此G1就想了个办法,我把在这个期间对对象引用的修改都记录动作都记录下来,有点像mysql的操作日志。

        • RSet

          • Remember Set,每个分区中维护一个RSet,主要记录其他分区引用本分区对象的关系,谁引用了我的对象

            • 如何辅助GC

              • YGC时,选定Y区的RSet作为根集,里面记录old->young的跨带引用,避免扫描整个old代区
              • mixed gc时,old代中每个分区记录old->old,young->old的RSet,不用扫描整个old分代区
        • CSet

          • Collection Set,GC要收集的Region的集合(任意分代),跨分区的扫描RSet
        • 停顿预测模型

          • 通过模型统计计算出的历史数据来预测本次回收需要选择的Region数量,尽量满足设置的目标
          • 通过XX:MaxGCPauseMillis参数设置用户期望的停顿时间,默认200ms
          • 衰减标准偏差为理论基础
      • GC模式

        • Young GC

          • E区无法分配内存(达到阈值)时启动,即E区和S区复制到Old区(MaxTenuringThreshold参数配置)或者另外一个S区,多线程并行执行

            YoungGC的回收过程如下:

            根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。
            处理Dirty card,更新RSet.
            扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
            拷贝扫描出的存活的对象到survivor2/old区
            处理引用队列,软引用,弱引用,虚引用(下一篇优化中会再讲一下这三种引用对gc的影响)



        • Mixed GC

          • 只回收老年代部分region,一般发生在YGC之后,目的是复用YGC扫描的GC Root,减少stw

          • 发生时机

            • G1MixedGCLiveThresholdPercent参数控制老年代分区中的存活对象比例,达到阈值这个分区会放在RSet中,默认45
            • G1HeapWastePercent参数控制,在一次younggc之后,可以允许的堆垃圾百占比,超过这个值就会触发mixedGC
          • 步骤

            • 1、初始标记:标记GC Roots,会STW,复用YoungGC的暂停时间,设置好所有分区的NTAMS值

            • 2、根分区扫描(RootRegionScan)

              • 和java程序并行执行,基于标记算法,对Survivor对象全部扫描标记为gcroot
            • 3、并发标记:从GC Root引用对象开始遍历对象树,标记出存活对象

            • 4、最终标记:会STW,标记出在3阶段发生变化的对象,同时处理STAB缓冲区;

            • 5、清除:STW,清除标记的垃圾对象,清理之后,将存活对象复制到其他可用分区,主要解决内存碎片问题

              • 1、对各个Region的回收价值和成本进行排序,根据用户设置的停顿时间执行清除计划

                比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内

              • 2、采用复制算法,将一个region中的存活对象复制到另一个空的region,这样好处在于不会产生碎片

      • 使用场景

        • 服务端垃圾收集器

        • 多处理器,内存偏大,一般大于6G以上

        • 需要低延迟的响应(停顿时间可控)

        • 存在以下情况可以尝试使用G1

          • Full GC 次数太频繁或者消耗时间太长
          • 对象分配的频率或代数提升(promotion)显著变化
          • 受够了太长的垃圾回收或内存整理时间(超过0.5~1秒)
      • 和CMS的区别

        • 停顿时间可控
        • 最终标记效率更高,G1只标记写屏障记录的对象,CMS Remark阶段扫描所有对象,STW时间更长
        • CMS清除阶段是并发的,G1是STW
      • 最佳实践配置

        • -XX:+UseG1GC
          -XX:MaxGCPauseMillis=200 //设置停顿时间,默认200
          -XX:INitiatingHeapOccupancyPercent=45 //设置整个堆使用率超过设置的值时启动Mix GC,默认45

        • 不要设置年轻代的大小

          通过-Xmn显式设置年轻代的大小,会干扰G1收集器的默认行为:

          G1不再以设定的暂停时间为目标,换句话说,如果设置了年轻代的大小,就无法实现自适应的调整来达到指定的暂停时间这个目标
          G1不能按需扩大或缩小年轻代的大小

        • 响应时间度量

          不要根据平均响应时间(ART)来设置-XX:MaxGCPauseMillis=n这个参数,应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间,记住,这个值是一个目标,但是G1并不保证100%的GC暂停时间都可以达到这个目标

        • -XX:ParallelGCThreads=n //垃圾收集器的并行阶段的垃圾收集线程数

        • -XX:ConcGCThreads=n //垃圾收集器并发执行GC的线程数

    • ZGC

    • Shenandoah

  • 三色标记法

    • 含义

      • 黑:对象和属性引用的对象已完成标记
      • 灰:对象被标记,但属性资源引用的对象没有标记完成
      • 白:对象没有被标记,回收对象
    • 问题

      • 漏标(两者缺一不可)

        • Mutator将黑对象引用指向白对象
        • Mutator删除灰对象到白对象的直接或者间接引用
      • 解决办法

        • 核心是解决其中一步即可

          • CMS 增量更新+写屏障
            黑对象新增白对象引用时通过写屏障记录下来,在重新标记阶段对记录的从新标记,即黑色对象变为灰色对象
          • G1 Shenandoah STAB+写屏障
            灰色对象删除了白色对象引用时,通过写屏障记录下来,然后重新标记阶段再次标记
          • ZGC 读屏障(待学习和补充)

XMind - Trial Version

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章