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

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

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

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

1. JVM垃圾回收算法

在这里插入图片描述

分代收集理论

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

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

为什么要分代收集:

因为对象的存活周期不一样,所以使用分代收集,不同的代收集不同存活周期的对象!
        

①:复制算法

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

        标记-复制 算法一般用在新生代,因为标记-复制算法只使用一半的内存空间,因为新生代对象朝生夕死的缘故,只需要付出少量的复制成本就可以完成垃圾收集。而老年代对象存活几率高,复制的成本很大,而且内存只能使用一般,所以不适用于老年代。

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

        

②:标记-清除算法

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

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

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

内存碎片的危害是什么?

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

③:标记-整理算法

        由于复制算法不适用于老年代,根据老年代的特点,有人提出了另外一种“标记-整理”(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)这两个垃圾收集器配合使用!

        

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

        三色标记算法是可达性分析算法底层的一种实现,存在于CMSG1垃圾收集器中。 在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生,可以使用三色标记来解决。

三色标记原理

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

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

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

	public class ThreeColorRemark {
	
	    public static void main(String[] args) {
	    	//初始标记 仅标记gcroot,a属于局部变量表中的gcroot
	        A a = new A();
	        
	        //开始做并发标记
	       
	       	// 并发标记,应用线程继续执行,先取出 B 对象中的 D,并暂存起来
	       	// 此时的GC线程,仍在进行可达性分析
	        D d = a.b.d;   // 1.读
	        
	        // 由于应用线程和GC线程并发执行
	        // 1. 此时应用线程把 B 对象中的 D 置为了null,断开了对D对象的引用
	        // 2. 那么GC线程并发执行可达性分析时,刚好发现B中的D断开了引用,
	        // 3. 于是把D置为了白色对象,把B置为黑色对象,因为B的所有引用已经扫描完毕了
	        a.b.d = null;  // 2.写  

			// 1. 然后应用线程继续执行,把D的引用赋值给了 A 对象(黑色对象)
			// 2. 那么这个D 对象就由白色的垃圾对象 变为了黑色可用的对象,
			// 3. 然而由于应用线程和GC线程并发执行, GC线程并不知道 D 对象已经变为了有用的对象
			// 4. 如果继续执行GC,清除D对象,就会出现系统错误。 这就是漏标的危害
	        a.d = d;       // 3.写
	    }
	}
	
	// A对象中引用了 B 对象
	class A {
	    B b = new B();
	    D d = null; 	
	}
	
	// B 对象中引用了 C 和 D 对象
	class B {
	    C c = new C();
	    D d = new D();
	}
	
	// C 对象
	class C {
	}
	// D 对象
	class D {
	}

三色标记过程分析:假如: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期间新增了对象引用。例如:上述代码中的a.d = d(应用线程把已断开引用的D对象新增一个引用,链接到A对象上),增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 在并发标记阶段,就将这个新插入的增量源头记录下来到保存一个集合里边。
      • 等并发标记结束之后, 在重新标记过程中将这些记录过的引用关系中的源头(黑色对象)为根, 重新扫描一次。 重新标记期间程序是stw状态,只有GC线程并发执行,所以不会再次产生漏标,且速度较快。
    • ②:原始快照(Snapshot At The Beginning,SATB
      • 原始快照主要针对的是GC期间引用关系被删除的操作。例如:上述代码中的a.b.d = null; (应用线程把B中的D对象的引用断掉,置为null,标识D为白色垃圾),原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个已废弃引用关系的白色对象(D对象)记录到一个容器里边。
      • 等并发标记结束之后, 在重新标记过程中把容器里边的白色对象直接标记为黑色(目的就是让这种对象在本轮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); // 在增量更新之后,把对象放入remark集合
  • 写前屏障实现原始快照SATB
    • 当对象B的成员变量的引用发生变化时,比如引用删除a.b.d = null,我们可以利用写屏障,在引用删除之前,将B原来成员变量的引用对象d记录下来
    • remark_set.add(old_value); // 在引用删除之前,把对象放入remark集合

对于读写屏障,以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%,这个就很厉害了,G1、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 都是安全的。


5. 记忆集和卡表

        记忆集和卡表的主要作用是解决GC垃圾收集时的跨代引用问题。在新生代做GCRoots可达性扫描过程中可能会碰到跨代引用的对象,这种如果又去对老年代再去扫描效率太低了。 为此,在新生代可以引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。事实上并不只是新生代、老年代之间才有跨代引用的问题, 所有涉及部分区域收集 (Partial GC) 行为的垃圾收集器, 典型的如G1ZGCShenandoah收集器, 都会面临相同的问题。

        垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引 用指针的全部细节。“卡表”是记忆集的一种实现,也是目前最常用的一种方式。关于卡表与记忆集的关系, 可以类比为Java语言中HashMapMap的关系

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值