JVM垃圾回收算法与垃圾收集器

 

 

JVM是经过分代收集理论进行垃圾回收的,即新生代和老年代选择的垃圾回收算法是不一样的:java

  • 新生代:标记-复制算法
  • 老年代:标记-清除、标记-整理算法等。

下面来看每一个算法的理论和应用:
                
        算法

1. JVM垃圾回收算法

在这里插入图片描述

分代收集理论

        当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不一样将内存分为几块。通常将java堆分为新生代和老年代,这样咱们就能够根据各个年代的特色选择合适的垃圾收集算法。这就是分代收集理论:安全

  • 在新生代中,每次收集都会有大量对象(近99%)死去,因此能够选择复制算法,只须要付出少许对象的复制成本就能够完成每次垃圾收集。
  • 而老年代的对象存活概率是比较高的,并且没有额外的空间对它进行分配担保,因此咱们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

为何要分代收集:服务器

由于对象的存活周期不同,因此使用分代收集,不一样的代收集不一样存活周期的对象!
        多线程

①:复制算法

        “复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。由于会复制并清理已使用的通常内存,因此也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效,但却要牺牲通常的内存空间。并发

        标记-复制 算法通常用在新生代,由于标记-复制算法只使用一半的内存空间,由于新生代对象朝生夕死的缘故,只须要付出少许的复制成本就能够完成垃圾收集。而老年代对象存活概率高,复制的成本很大,并且内存只能使用通常,因此不适用于老年代。jvm

如图所示:
在这里插入图片描述ide

        

②:标记-清除算法

算法分为 “标记“ 和 “清除” 两个阶段。标记存活的对象,清除未被标记的对象。高并发

标记-清除算法带来的两个问题:oop

  • ①:效率问题(若是被标记的对象太多,效率不高)
  • ②:空间问题(标记清除后会有大量的内存碎片)

内存碎片的危害是什么?

        空间碎片太多可能会致使,当程序在之后的运行过程当中须要分配较大对象时没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。
在这里插入图片描述

③:标记-整理算法

        因为复制算法不适用于老年代,根据老年代的特色,有人提出了另一种“标记-整理”(Mark-Compact)算法。该算法是在标记-清除的基础上,增长了整理的操做,把碎片化的空间整理为隔离的。后续步骤不是直接对可回收对象回收,而是让全部存活的对象向一端移动,而后直接清理掉端边界之外的内存。

这种算法克服了复制算法的空间浪费问题,同时克服了标记清除算法的内存碎片化的问题;
在这里插入图片描述
        

2. 垃圾收集器

        垃圾回收算法是jvm内存回收过程当中具体的、通用的方法。而垃圾收集器是jvm内存回收过程当中具体的执行者,即各类GC算法的具体实现。

        目前为止尚未万能的垃圾收集器,咱们只能根据具体场景来选择合适的垃圾收集器。这也是目前垃圾收集器种类繁多的缘由!!各类垃圾收集器的组合使用以下图:

在这里插入图片描述
Epsilon、Shenandoah:这两个收集器是redHat开发的,其中ShenandoahG1的加强版本,因为他们不是Oracle公司开发的,且使用的极少,本文暂不介绍!

        

①:Serial 收集器

JVM参数设置: -XX:+UseSerialGC -XX:+UseSerialOldGC

单线程收集器,他不只只有一条GC线程,在GC时还必须中止其余全部的工做线程(STW),不多使用。

注意:

  • ①:虽然是单线程,可是效率低但简单而高效,相比于其余线程,没有线程上下文切换的开销!
  • ②:Serial收集器的新生代采用复制算法,老年代采用标记-整理算法。
    在这里插入图片描述

②:Parallel Scavenge 收集器

JVM参数设置:-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)

JDK 1.8默认使用 Parallel垃圾收集器(年轻代和老年代都是),这个垃圾收集器没法与CMS垃圾收集器配合使用!对于堆内存2-3个G的状况,使用Parallel Scavenge收集器足够应对!

多线程收集器,是Serial收集器的多线程版本,默认的收集线程数跟cpu核数相同,固然也能够用参数- XX:ParallelGCThreads指定收集线程数,可是通常不推荐修改。

注意:
①:Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。GC总时间相对于CMS收集器较短!
②:Parallel Scavenge收集器新生代采用复制算法,老年代采用标记-整理算法。
在这里插入图片描述

③:ParNew 收集器

JVM参数设置:-XX:+UseParNewGC

        ParNew收集器主要做用和parallel收集器相似,区别主要在于ParNew收集器能够配合CMS收集器使用。除了Serial收集器外,只有它能与CMS收集器配合工做。配合工做时,通常ParNew负责年轻代垃圾收集,CMS负责老年代垃圾收集!这种组合是不少公司都在用的一种垃圾收集组合

  • ParNew收集器新生代使用复制算法,老年代采用标记-整理算法
    在这里插入图片描述

④:CMS收集器(重点)

JVM参数设置:-XX:+UseConcMarkSweepGC(old)

CMS相对于parallel 收集器的区别?

  • CMS (Concurrent Mark Sweep)收集器是只有老年代才能用的垃圾收集器!
  • CMS收集器使用的是 标记-清除 算法,parallel 收集器新生代使用 复制 算法,老年代采用 标记-整理 算法
  • 若是jvm堆内存过大(8G左右),使用parallel收集器时,GC时须要较长时间进行 标记-整理 ,在此期间,用户线程是stw的,很大程度上下降了用户体验;而CMSparallel 的多线程GC过程分为多个阶段,在最耗时的标记阶段使用并发标记,让用户线程和GC线程同时执行。因此在应对大内存的jvm时,明显CMS收集器使得用户体验更好
  • 相对于Parallel收集器,CMS使用较短期的STW,换取用户的体验,由于他把最耗时的标记过程,改为了GC线程和用户线程并行,但因为CMS拆分了GC过程,因此总体GC时间要长于Parallel,但stw时间更短。因此cms主要是提高用户体验的,其实gc效率不如Parallel

工做流程以下
在这里插入图片描述

  • ①:初始标记:暂停其余线程(STW),只标记gc roots直接引用的对象,速度很快!由于初始标记并不标记gc root的全部引用。
  • ②:并发标记:根据上一步标记的对象,根据可达性分析算法找整个对象引用链,此过程比较耗时,因此采用用户线程和GC线程并发执行,不会STW,保证了用户体验,这点也是cms收集器饱受青睐的缘由之一。但正由于并发标记,用户线程也在执行,就可能会出现多标或漏标的问题。
  • ③:从新标记:从新标记阶段就是为了修正并发标记期间由于用户程序继续运行而致使多标或漏标的问题。这个阶段主要用到三色标记的更新算法(增量更新、原始快照),速度比初始标记慢一点,但远比并发标记时间短。
  • ④:并发清理:开启用户线程和GC线程并发执行,提升了速度,但同时带来了和并发标记一样的问题,这种问题主要经过三色标记算法来解决的(下文会有讲解)
  • ⑤:并发重置:重置本次GC过程当中的标记数据。

CMS收集器的优缺点

  • 优势:
    • ①:并发收集,低停顿,用户体验较好
  • 缺点:
    • ①:GC线程和用户线程并发执行,会存在cpu上下文切换,影响GC效率;
    • ②:并发清理阶段,用户线程可能会产生新的垃圾对象。也就是说清理完成后,本来已清除干净的位置上仍是有用户线程产生的新垃圾,这个垃圾被称为浮动垃圾。浮动垃圾不影响程序运行,本次GC过程没法处理浮动垃圾,要等到下次GC处理。
    • ③:垃圾回收算法用的 标记-清除 ,会有空间碎片产生,可使用数- XX:+UseCMSCompactAtFullCollection可让jvm在执行完标记清除后再作整理,整理是也会stw,但时间较短!
    • ④:在并发标记和并发清理阶段,一边GC回收,用户程序一边执行,若是用户线程产生了一个大对象,会直接进入老年代,而此时的老年代尚未GC回收完毕,已经没有足够的内存去接收这个大对象了。 这时就会出现 "concurrent mode failure"(并发修改失败),此时会stop the world全部用户线程,专心作垃圾收集,可是用的是serial old串行垃圾收集器来回收,这个串行垃圾收集器效率至关低!代价比较大,尽可能避免!

CMS的相关核心参数

--xx-xx三种jvm参数前缀有什么不一样:x的个数越多,表明这个参数的版本支持变数越高,有可能jdk8适用,jdk9就废除掉了!

  • -XX:+UseConcMarkSweepGC:启用cms
  • -XX:ConcGCThreads:并发的GC线程数
  • -XX:+UseCMSCompactAtFullCollection:FullGC以后作压缩整理(减小碎片)
  • -XX:CMSFullGCsBeforeCompaction:多少次FullGC以后压缩一次,默认是0,表明每次FullGC后都会压缩一次
  • -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92%这个参数能够防止concurrent mode failure
  • -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收百分比(-XX:CMSInitiatingOccupancyFraction设定的值),若是不配置此参数,-XX:CMSInitiatingOccupancyFraction设定的值无效!由于jvm默认会根据gc状况动态调整回收的百分比,相似于元空间的自动扩容、缩容!
  • -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,下降CMS GC标记阶段(也会对年轻代一块儿作标记,若是在minor gc就干掉了不少对垃圾对象,标记阶段就会减小一些标记时间)时的开销,通常CMS的GC耗时 80%都在标记阶段
  • -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  • -XX:+CMSParallelRemarkEnabled:在从新标记的时候多线程执行,缩短STW;

问题一:“concurrent mode failure”(并发修改失败)怎么预防?

        因为默认老年代空间达到92% 就会full GC,固然这个值是能够经过参数调的。在并发标记或并发清理阶段,若是不断有大对象进入老年代,老年代剩余的8%空间很快会被填满,此时就会出现"concurrent mode failure"。咱们能够经过 -XX:CMSInitiatingOccupancyFraction=80 参数来调整老年代的full GC发生时机为80%,让老年代发生GC时还有更多空间存储新生代存活的大对象!

        
问题二:"Parallel 和CMS收集器使用场景

        JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代)。
若是内存较大(超过4个G,8个G之内,只是经验值),系统对停顿时间比较敏感,咱们可使用ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)这两个垃圾收集器配合使用!

        

三色标记算法解决漏标的原理

        在并发标记的过程当中,由于标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的状况就有可能发生,可使用三色标记来解决。

三色标记原理

三色标记把可达性分析遍历对象过程当中遇到的对象, 按照“是否访问过”这个条件标记成如下三种颜色:

  • ①:黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的全部引用都已经扫描过。 黑色的对象表明已经扫描 过, 它是安全存活的, 若是有其余对象引用指向了黑色对象, 无须从新扫描一遍。 黑色对象不可能直接(不通过 灰色对象) 指向某个白色对象。
  • ②:灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用尚未被扫描过。
  • ③:白色: 表示对象还没有被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 全部的对象都是白色的, 若 在分析结束的阶段, 仍然是白色的对象, 即表明不可达。

如图所示
在这里插入图片描述
三色标记过程分析:假如:A类中包含了B ,B类中包含了C和D。

  • ①:可达性分析算法会先根据gc roots的局部变量a去找,a指向了A类,就扫描了A中的全部对象(此例中只有一个B),那么A就会被标为黑色。回收时不会管黑色对象,由于已经分析完了。
  • ②:而后根据可达性分析算法,开始扫描B,B中包含了C和D,若是 此时恰好扫描完C,还没开始扫描D时。当前的B为灰色,表明至少存在一个引用尚未被扫描过。
  • ③:因为上一步C已经被扫描,且没有更多引用。因此为黑色。而D在那个时机中还没被扫描,为白色。

        刚开始默认都是白色对象,扫描标记完成后,黑色和灰色对象不会被回收,白色会回收。明白了三色标记原理后,来看一下具体是如何解决漏标问题的!

问题三:并发标记阶段的多标和漏标怎么解决?

  • 多标:会产生浮动垃圾。因为并发运行的用户线程结束,会改变某些已标记过的对象的状态,好比gc root被销毁,那么会有部分GC线程已扫描过的黑色对象转变为白色对象,那么本轮GC不会回收这些浮动垃圾,留着下一次GC进行回收,浮动垃圾并不影响垃圾回收的正确性。

  • 漏标:漏标会致使被引用的对象被当成垃圾误删除,这是严重bug,必须解决。产生缘由:并发执行中,用户线程把某些白色对象的引用指向了GC已扫描过的黑色对象,那么最初的白色对象也变成黑色对象了,而GC线程并不知道这个过程,会删除有用的对象。

  • 漏标有两种解决方案:

    • ①:增量更新(Incremental Update)
      • 所谓增量就是GC期间新增了对象引用。增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的增量记录下来到保存一个集合里边。
      • 等并发标记结束以后, 在从新标记过程当中中将这些记录过的引用关系中的黑色对象为根, 从新扫描一次。 从新标记期间程序是stw状态,只有GC线程并发执行,因此不会再次产生漏标,且速度较快。
    • ②:原始快照(Snapshot At The Beginning,SATB)
      • 原始快照主要针对的是GC期间引用关系被删除的操做。就是当灰色对象要删除指向白色对象的引用关系时, 就将这个引用关系记录到一个容器里边。
      • 等并发标记结束以后, 在从新标记过程当中把容器里边的白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候从新扫描,这个对象也有多是浮动垃圾),
    • 增量更新原始快照两种方案的区别在于:
      • 增量更新须要在从新标记阶段以黑色对象为根,在深度扫描一次,效率可能会有所影响!
      • 原始快照在从新标记阶段直接把白色对象变成黑色,不须要深度扫描,可是可能这个对象并无被引用,产生浮动垃圾!不过下次GC就会清理,不影响

写屏障

         以上不管是增量更新仍是原始快照虚拟机的记录操做都是经过写屏障实现的。由于想要增长引用或者删除引用,必有引用赋值操做这一步,写屏障就是利用AOP的理念,在引用赋值操做先后,加入一些记录处理,收集这些将要赋值的引用,并保存起来!

给某个对象的成员变量赋值时,其底层代码大概长这样:

/**
* @param field 某对象的成员变量,如 a.b.d 
* @param new_value 新值,如 null
*/
void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; // 赋值操做
}

所谓的写屏障,其实就是指在赋值操做先后,加入一些处理(能够参考AOP的概念):

void oop_field_store(oop* field, oop new_value) {  
    pre_write_barrier(field);          // 写屏障-写前操做
    *field = new_value; 			// 赋值操做
    post_write_barrier(field, value);  // 写屏障-写后操做
}
  • 写屏障实现增量更新
    • 当对象A的成员变量的引用发生变化时,好比新增引用a.d = d,咱们能够利用写屏障,在增量更新以后,将A新的成员变量引用对象d记录下来
    • remark_set.add(new_value); // 在增量更新以后,记录新引用的对象
  • 写屏障实现原始快照SATB
    • 当对象B的成员变量的引用发生变化时,好比引用删除a.b.d = null,咱们能够利用写屏障,在引用删除以前,将B原来成员变量的引用对象d记录下来
    • remark_set.add(old_value); // 在引用删除以前,记录原来的引用对象

对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案以下:

  • CMS:写屏障 + 增量更新
  • G1Shenandoah:写屏障 + 原始快照SATB
  • ZGC:读屏障

 

⑤:G1垃圾收集器

JVM参数设置:-XX:+UseG1GC

JDK 1.9默认使用 G1

        G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高几率知足GC停顿时间要求的同时,还具有高吞吐量性能特征.G1垃圾收集器摒弃了分代收集理念,只保留了年轻代和老年代的概念,但物理上已经不存在了,而是以(能够不连续)Region的形式来存储对象。

Forget 分代收集:
在这里插入图片描述
Region的形式来存储对象:每个小块能够看作是一个Region在这里插入图片描述

G1垃圾收集器的特色?

  • ①:可自定义stw时间
    • G1垃圾收集器主要针对大内存的机器,能够设置GC停顿时间(默认200ms)用户可控(经过参数"- XX:MaxGCPauseMillis"指定),以极高的几率知足GC停顿的同时,也保证了高吞吐量的特征。G1垃圾收集器在逻辑上保留了年轻代、老年代的概念,但在物理上已经抛弃了这些,年轻代和老年代区域能够任意转换。
  • ②:年轻代自动扩容
    • 年轻代默认占堆空间的5%(能够经过-XX:G1NewSizePercent设置新生代初始占比),在系统运行中,JVM会不停的给年轻代增长更多的Region,可是最多新生代的占比不会超过60%(能够经过-XX:G1MaxNewSizePercent进行调整),这也是与其余垃圾收集器的不一样之处!好比:堆大小为4096M,那么年轻代默认占据200MB左右的内存,对应大概是100个Region,每一个Region大小为2M
  • ③:Region存储机制
    • G1垃圾收集器将堆分为多个大小相等的独立区域(Regin),jvm最多存在2048Regin,通常Region大小等于堆大小除以2048,若是堆内存大小是4096M,那每一个Region大小默认为2M。可以使用-XX:G1HeapRegionSize手动指定Region大小。年轻代中的EdenSurvivor对应的region也跟以前同样,默认8:1:1,假设年轻代如今有1000个region,eden区对应800个,s0对应100个,s1对应100个。一个Region可能以前是年轻代,若是Region进行了垃圾回收,以后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。
  • ④:专门处理大对象的Humongous区
    • G1垃圾收集器对于对象回收规则和其余垃圾收集器同样,惟一不一样的是对大对象的处理。之前的垃圾收集器会根据动态年龄判断等把大对象放入老年代。而G1则有专门的处理大对象的Regin---->Humongous区。若是一个对象超过了一个Regin大小的50%,则会进入Humongous区,一个Humongous放不下,会横跨多个Humongous放置这个对象!Full GC的时候除了收集年轻代和老年代以外,也会将Humongous区一并回收。
      用处:能够节约老年代的空间,避免由于老年代空间不够的GC开销。
  • ⑤:采用复制算法回收垃圾
    • g1垃圾回收算法采用复制算法,将一个region中的存活对象复制到另外一个空的region中,并清空原region中的对象。由于G1中年轻代和老年代都是以region进行存储的,因此年轻代和老年代均可以使用复制算法! 这种不会像CMS那样回收完由于有不少内存碎片还须要整理一次,G1采用复制算法回收几乎不会有太多内存碎片

 

G1的垃圾回收过程

G1由于在物理上已经不区分年轻代、老年代,因此逻辑上的年轻代,老年代都用的同一个垃圾收集器G1。
在这里插入图片描述

  • 初始标记: 同CMS的初始标记。
  • 并发标记: 同CMS的并发标记。
  • 最终标记: 同CMS的从新标记。只不过G1使用原始快照解决漏标问题,而CMS使用增量更新解决漏标问题
  • 筛选回收: 筛选回收阶段和CMS不一样,CMS中用户线程和GC线程并发清除,不会stw;G1只有GC线程工做,此时会stw。筛选回收会首先对regin的回收成本作计算排序,再根据用户指望的GC停顿时间(默认200ms)来制定回收计划,根据回收计划回收垃圾

 

G1的垃圾收集分类

  • ①:YoungGC
    • G1的eden区默认占堆的5%YoungGC并非说Eden区满了就马上触发,G1会计算如今回收Eden须要多长时间,若是时间远小于用户设定的指望时间(使用-XX:MaxGCPauseMills设定),就会给Eden区扩容,直到扩容后的Eden区再次放满,再次计算。。。直到回收须要时长约等于用户设定的指望停顿时间,此时才会触发YoungGC!
  • ②:MixedGC
    • MixedGC并非FullGC,MixedGC的发生条件:经过-XX:InitiatingHeapOccupancyPercent设置老年代的占用比,默认是45%,若是达到这个比例就触发MixedGC,会回收Young、部分Old、Humongous区的对象。好比:堆默认有2048个region,若是有接近1000个region都是老年代的region,则可能就要触发MixedGC了,MixedGc使用复制算法。须要把各个region中存活的对象拷贝到别的region里去,拷贝过程当中若是发现没有足够的空region可以承载拷贝对象就会触发一次真正的Full GC
  • ③:FullGC
    • 中止系统程序,而后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是很是耗时的。(Shenandoah优化成多线程收集了)

G1收集器参数设置

  • -XX:+UseG1GC:使用G1收集器
  • -XX:ParallelGCThreads:指定GC工做的线程数量
  • -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区,默认2M
  • -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
  • -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
  • -XX:G1MaxNewSizePercent:新生代内存最大空间
  • -XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),其实就是以前说的动态年龄判断。Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
  • -XX:MaxTenuringThreshold:最大年龄阈值(默认15)
  • -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),好比咱们以前说的堆默认有2048个region,若是有接近1000个region都是老年代的region,则可能就要触发MixedGC了
  • -XX:G1MixedGCLiveThresholdPercent:region中的存活对象低于这个值时才会回收该region(默认85%) ,若是超过这个值,存活对象过多,回收的的意义不大。
  • -XX:G1MixedGCCountTarget:在一次回收过程当中指定作几回筛选回收(默认8次),在最后一个筛选回收阶段能够回收一会,而后暂停回收,恢复系统运行,一会再开始回收,这样可让系统不至于单次停顿时间过长。这个过程至关于把筛选回收阶段切分为 GC线程 – 用户线程 – GC线程,注意这过程不是并发,而是串行
  • -XX:G1HeapWastePercent(默认5%):gc过程当中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其余Region,而后这个Region中的垃圾对象所有清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会当即中止混合回收,意味着本次混合回收就结束了。

问题一:为何g1筛选回收阶段不作成和CMS用户线程和GC线程并发呢?

        CMS用户线程和GC线程并发的最主要做用就是防止STW的时间过长而设计。但由于g1垃圾收集器的STW时间是用户可控的,就解决了CMS并发收集存在的问题。
在问题已解决的同时,关闭用户线程将大幅度提升GC效率,即知足了GC停顿,还保证了GC的高吞吐量!

        
问题二:用户能够随意设置stw停顿时间吗?为何?

  • ①:毫无疑问, 能够由用户指按期望的停顿时间是G1收集器很强大的一个功能, 设置不一样的指望停顿时间, 可以使得G1在不 同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。 不过, 这里设置的“指望值”必须是符合实际的。
  • ②:这个停顿时 间再怎么低也得有个限度。 它默认的停顿目标为两百毫秒, 通常来讲, 回收阶段占到几十到一百甚至接近两百毫秒都很 正常, 但若是咱们把停顿时间调得很是低, 譬如设置为二十毫秒, 极可能出现的结果就是因为停顿目标时间过短, 导 致每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐渐跟不上分配器分配的速度, 致使垃圾慢慢堆 积。 极可能一开始收集器还能从空闲的堆内存中得到一些喘息的时间, 但应用运行时间一长就不行了, 最终占满堆引起 Full GC反而下降性能。
  • ③:因此一般把指望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。保证年轻代gc别太频繁的同时,还得考虑 每次年轻代gc事后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.

        
问题三:什么场景适合使用G1收集器?

  • ①:8GB以上的堆内存 。由于G1收集器的底层算法是比CMS要复杂的。若是在低内存中使用G1,原本垃圾也不是不少,算法还要占用必定时间。可能得不偿失,因此g1要物尽其用,尽可能在大内存中使用!
  • ②:对停顿时间要求高,注重用户体验的场景

        好比像kafka这种支持高并发的系统,每秒处理几万甚至几十万消息时很正常的,通常来讲部署kafka须要用大内存机器(好比64G),那么年轻代就有40多个G,普通的Young GC 须要扫描40G空间花费的时间是很是多的,可能最快也要几秒钟。

        按kafka这个并发量,放满三四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会由于young gc卡顿几秒钟无法处理新消息,显然是不行的 ,那么对于这种状况如何优化呢?

        咱们可使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假设50ms可以回收三到四个G内存,而后50ms的卡顿其实彻底可以接受,用户几乎无感知,那么整个系统就能够在卡顿几乎无感知的状况下一边处理业务一边收集垃圾。

        G1天生就适合这种大内存机器的JVM运行,能够比较完美的解决大内存垃圾回收时间过长的问题。

        
问题四:在并发标记产生的漏标中,为何G1用(原始快照)SATB?CMS用增量更新?

        在解决漏标问题时,增量更新须要以黑色对象为根,在经过gc root作一次深度扫描,这其中还可能包括跨代引用等状况,这个过程是挺耗费时间的。而原始快照则只须要把集合中的白色对象引用置为黑色,默认这个对象是有用的,不能被回收,即便它多是浮动垃圾。这种简单粗暴的方式,虽然可能产生多的浮动垃圾,但不须要深度扫描。

         G1的不少对象都位于不一样的regin中,这个regin是有不少个的,若是使用增量更新要从不少个regin中找gc root的引用关系,很是耗时。而使用原始快照不须要在从新标记阶段再次深度扫描对象,只是简单标记,等到下一轮GC 再深度扫描。因此G1使用原始快照相对于增量更新效率会高。而CMS使用增量更新,由于CMS就一块老年代区域,深度扫描的话影响也不是很大!

 

⑥:ZGC垃圾收集器

ZGC是一款JDK 11中新加入的具备实验性质的低延迟垃圾收集器,在目前的jdk8中并不适用!
在这里插入图片描述
ZGC的特色

  • ①:支持TB量级的堆内存,好像目前能支持到16TB吧,比G1更大
  • ②:GC停顿时间不超过10ms,且不随堆内存增大而增大!由于ZGC中全部的垃圾收集阶段几乎都是并发执行!
  • ③:最坏状况下GC吞吐量(垃圾回收总时间)不超过原时间的15%,这个就很厉害了,G一、CMS都是经过延长回收时间来增长用户体验的!
  • ④:ZGC完全抛弃了分带概念,再也不分带,由于分代实现起来麻烦,做者就先实现出一个比较简单可用的单代版本
  • ⑤:ZGC也是基于Region来实现内存布局的,分为大、中、小三类
    • 小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
    • 中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
    • 大型Region(Large Region) : 容量不固定, 能够动态变化, 但必须为2MB的整数倍, 用于放置4MB或 以上的大对象。

 

ZGC的运做过程
在这里插入图片描述

  • ①:并发标记: 与G1同样,并发标记是遍历对象图作可达性分析的阶段,它的初始标记 (Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不一样的是, ZGC的标记是在指针上而不是在对象头上。该阶段会更新颜色指针
  • ②:并发预备重分配: 回收的准备阶段,此阶段统计得出要回收那些regin,用这些refin组成重分配集(relocation set)。
  • ③:并发重分配: 把预分配算出来的重分配集的regin,复制到新的空regin上,并为重分配集中的每个regin维护一个转发表,记录着旧对象到新对象的转发关系。
    若是用户线程此时并 发访问了位于重分配集中的对象,此次访问将会被预置的内存屏障(读屏障)所截获,而后当即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指 针的“自愈”(Self-Healing)能力

并发重分配过程

  • ①:先把分配集中的对象复制到新的regin中去。
  • ②:原来对象的引用并不会立刻更新,这个更新是惰性更新。
  • ③:当并发的用户线程用到这个对象后才更新,这个过程使用读屏障来实现。当用户线程要用这个对象时,经过读屏障更新这个对象的引用到新的regin中,读屏障利用相似AOP的理论操做的。
    • 读屏障怎么知道新的对象地址呢?
      并发重分配在复制对象时会维护一个转发表,经过转发表得到!
  • ④:并发重映射: 把重分配集中的旧对象的引用指向并发重分配过程新分配的对象空间,通常在下一次gc中执行,由于本次GC,已经由并发重分配中的读屏障处理过了。

        
问题:ZGC和G1在清理垃圾阶段的区别是什么?

        zgc和g1的最大区别是在筛选回收阶段,G1是GC线程并发执行清理,此时STW,修改对象引用很方便。ZGC是GC执行清理时和用户线程并发操做,没有stw,复杂度很高

颜色指针

        以下图所示,ZGC的核心设计之一。之前的垃圾回收器的GC信息都保存在对象头中, 而ZGC的GC信息保存在指针中。

在这里插入图片描述
颜色指针的三大优点:

  1. 一旦某个Region的存活对象被移走以后,这个Region当即就可以被释放和重用掉,而没必要等待整个堆中全部指 向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。
  2. 颜色指针能够大幅减小在垃圾收集过程当中内存屏障的使用数量,ZGC只使用了读屏障。
  3. 颜色指针具有强大的扩展性,它能够做为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数 据,以便往后进一步提升性能。

 

 

3. 如何选择垃圾收集器

  1. 优先调整堆的大小让服务器本身来选择
  2. 若是内存小于100M,使用串行收集器
  3. 若是是单核,而且没有停顿时间的要求,串行或JVM本身选择
  4. 若是容许停顿时间超过1秒,选择并行或者JVM本身选
  5. 若是响应时间最重要,而且不能超过1秒,使用并发收集器
  6. 4G如下能够用parallel,4-8G能够用ParNew+CMS,8G以上能够用G1,几百G以上用ZGC

 

4. 安全点与安全区域

安全点

        就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是肯定的,这样JVM就能够安全的进行一些操做,好比GC等,因此GC不是想何时作就当即触发的,是须要等待全部线程运行到安全点后才能触发。若是马上挂起全部用户线程,可能会破坏某些用户线程的原子性,好比:i++、jvm底层程序计数器的跳转等。

        大致实现思想是当垃圾收集须要中断线程的时候, 不直接对线程操做, 仅仅简单地设置一个标志位, 各个线程执行过程 时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就本身在最近的安全点上主动中断挂起。 轮询标志的地方和 安全点是重合的。

这些特定的安全点位置主要有如下几种:

  1. 方法返回以前
  2. 调用某个方法以后
  3. 抛出异常的位置
  4. 循环的末尾
  5. 调用某个方法以前

        
安全区域

        若是一个线程处于 Sleep 或中断状态,它就不能扫描安全点,响应 JVM 的中断请求。那么他周围的一片区域都是称为安全区域,这个区域的引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

相关文章
相关标签/搜索