垃圾回收算法二(jdk7-jdk13涉及到的具体垃圾收集器)
具体讲具体的收集器之前,先说明一些垃圾收集器中涉及到是一些名词概念:
1、根节点枚举和OopMap
前面介绍过GC Roots的概念,每次从GC Roots开始检查引用链的时候,都是要让所有用户线程暂停,GC Roots里的引用对象太多,如恒河沙数,一个不漏地从方法区、栈区等GC Roots中的引用开始查找这将是一个非常耗时的过程,所以,解决方案是从外部记录下类型信息,生成映射表,在HotSpot中把这种映射表称之为OopMap,
实现这种功能,需要虚拟机的解释器和JIT编译器支持,由他们来生成OopMap。生成这样的映射表一般有两种方式:
每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”;
为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。
总而言之,GC开始的时候,就通过OopMap这样的一个映射表知道,在对象内的什么偏移量上是什么类型的数据,而且特定的位置记录下栈和寄存器中有哪些位置是对应的引用。
2、安全点
上面讲到了为了快点进行可达性的分析,使用了一个引用类型的映射表,可以快速的知道那些对象是可达的,那些是不可达的。
但是随着而来的又有一个问题,就是在方法执行的过程中, 可能会导致引用关系发生变化,那么保存的OopMap就要随着变化。如果每次引用关系发生了变化都要去修改OopMap的话,这又是一件成本很高的事情。
所以这里就引入了安全点的概念。什么是安全点?OopMap的作用是为了在GC的时候,快速进行可达性分析,所以OopMap并不需要一发生改变就去更新这个映射表。只要这个更新在GC发生之前就可以了。所以OopMap只需要在预先选定的一些位置上记录变化的OopMap就行了。这些特定的点就是SafePoint(安全点),由此也可以知道,程序并不是在所有的位置上都可以进行GC的,只有在达到这样的安全点才能暂停下来进行GC。
既然安全点决定了GC的时机,那么安全点的选择就至为重要了。安全点太少,会让GC等待的时间太长,太多会浪费性能。一般会在如下几个位置选择安全点:
循环的末尾
方法临返回前 / 调用方法的call指令后
可能抛异常的位置
3、增量更新和原始快照
当垃圾回收的线程和用户的线程并发执行的时候,垃圾线程从GC Roots开始扫描,扫描过的对象,又被用户线程改变了引用关系,会造成本应该标记存活的对象,被错误标记为垃圾对象的问题!由此分别产生了两种解决这个问题的方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)
增量更新: 当从存活对象插入新的指向原垃圾对象的引用的时候,就将这个新插入的引用记录下来,等并发扫描结之后,再将这些记录过的引用关系中的存活对象为根,重新扫描一次。
原始快照:当删除从一个存活对象指向原本存活对象的引用的时候,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的第一个存活对象为根,重新扫描一次。
具体垃圾收集器
1、Serial收集器
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK1.3.1之前)是HotSpot虛拟机新生代收集器的唯一选择。大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器,但它的”单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。Serial收集器虽然老,但是在客户端仍然推荐使用的,因为它高效,简单,额外内存消耗最小的收集器。

对应的参数:
-XX:+UseSerialGC
使用串行回收器进行回收,这个参数会使新生代和老年代都使用串行回收器,新生代使用复制算法,老年代使用标记-整理算法。Serial收集器是最基本、历史最悠久的收集器,它是一个单线程收集器。一旦回收器开始运行时,整个系统都要停止。Client模式下默认开启,其他模式默认关闭。
-XX: SurvivorRatio
-XX:SurvivorRatio=6 ,设置的是Eden区与一个Survivor区的比值是6:1,Eden为6, 两个Survivor为2, Eden占新生代的6/8,也就是3/4, 每个Survivor占1/8,两个占1/4
-XX:PretenureSizeThreshold
-XX:PretenureSizeThreshold=1024*1024*10超过这个值的时候,对象直接在老年代里分配内存,默认值是0,意思是不管多大都是先在eden中分配内存
-XX: HandlePromotionFailure
-XX: HandlePromotionFailure=true/false,空间分配担保的开关:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看-XX:+HandlePromotionFailure设置是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。
注意:在JDK 6 Update 24之后,-XX:HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。
2、 ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。

说到ParNew,不得不提前说一下CMS:
在JDK 5发布时,HotSpot推出了一款具有划时代意义的垃圾收集器一CMS收集器。 首次实现了让垃圾收集线程与用户线程(基本上)同时工作。激活CMS的参数:-XX:+UseConcMarkSweepGC激活CMS后,默认使用CMS+ParNew的组合做垃圾收集工作的。JDK9后,ParNew收集器的开关配置:-XX:+UseParNewGC也被取消了!可以说ParNewGC是第一款从HotSpot中退出舞台的垃圾收集器。
对应的参数:-XX:+UseParNewGC
Parallel是并行的意思,ParNew收集器是Serial收集器的多线程版本,使用这个参数后会在新生代进行并行回收,老年代仍旧使用串行回收。新生代S区任然使用复制算法。操作系统是多核CPU上效果明显,单核CPU建议使用串行回收器。打印GC详情时ParNew标识着使用了ParNewGC回收器。默认关闭。
补充:
jvm运行模式有两种:1.客户端模式,对应的参数:-client2.服务端模式,对应参数:-server注意很多版本,本身就不在区别客户端模式和服务端模式,就有服务端模式。
3、 Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记复制算法实现的收集器,也是能够并行收集的多线程收集器。Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;
而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput) 。这里的吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
Parallel Scavenge收集器相关参数:
-XX:+UseParallelGC
代表新生代使用Parallel收集器,老年代使用串行收集器。Parallel Scavenge收集器在各个方面都很类似ParNew收集器,它的目的是达到一个可以控制的吞吐量。Server模式默认开启,其他模式默认关闭。
-XX: MaxGCPauseMillis参数
此参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-XX: GCTimeRatio参数
此参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5% (即1/(1+19)),
默认值为99,即允许最大1% (即1/(1+99)) 的垃圾收集时间。
-XX: +UseAdaptiveSizePolicy参数
这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn) 、Eden与Surivor区的比例 (-XX: SurvivorRatio) 、升级到老年代对象大小(-XX: PretenureSizeThreshold) 等细节参数了,
虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
4、Serial Old
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虛拟机使用。有的时候你会看到这个名字PS MarkSweep默认的实现实际上是一层皮,它底下真正做mark-sweep-compact工作的代码和serial old是共用同一份代码的。
而在Server模式有两大用途:
(A)、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
(B)、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
5、Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记整理算法实现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old 收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。Serial Old 收集器的单线程就是整个收集系统的瓶颈。
直到Parallel Old收集器出现后,"吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

-XX:+UseParallelOldGC:指定使用Parallel Old收集器;
6、CMS
CMS (Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。从名字(包含"Mark Sweep")上就可以看出CMS收集器是基于标记清除算法实现的,它的运作过程相对于前面几种收集器来说要复杂一些,整个过程分为四个步骤,包括:
1)初始标记(CMS initial mark)
2)并发标记(CMS concurrent mark)
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要"Stop The World"。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行; 重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程可以看做是同时执行的。

CMS是一款优秀的收集器, 它最主要的优点在名字上已经体现出来:并发收集、低停顿 。CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试, 但是它还远达不到完美的程度,至少有以下三个明显的缺点:
首先,对计算机运算资源非常敏感
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分处理器的计算能力而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4, 也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。
其次,CMS无法处理“浮动垃圾”,有可能出现“Concurrent Mode Failure”,导致“Stop The World”在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行,自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure) ,这时候虚拟机将不得不启动后备预案:冻结用户线程的执行此时就“Stop The World”,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
最后,CMS使用“标记-清理”算法,不会移动存活对象,会产生过多的内存空间碎片,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。在CMS收集器不得不进行Full GC时默认会进行内存碎片的合并整理。
对应配置参数:
1、启用CMS:-XX:+UseConcMarkSweepGC
2、CMS默认启动的回收线程数目是 (ParallelGCThreads + 3)/4) ,如果你需要明确设定,可以通过-XX:ParallelCMSThreads=20来设定,其中ParallelGCThreads是年轻代的并行收集线程数
3、CMS是不会整理堆碎片的,因此为了防止堆碎片引起full gc,通过会开启CMS阶段进行合并碎片选项:-XX:+UseCMSCompactAtFullCollection(默认开启,JDK 9废弃),开启这个选项一定程度上会影响性能,也许可以通过配置适当的-XX:CMSFullGCsBeforeCompaction=10(JDK 9废弃),来调整性能。在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做合并碎片。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做合并碎片。 把CMSFullGCsBeforeCompaction配置为10,就会变成每隔10次真正的full GC才做一次合并碎片。
4.为了减少第二次暂停的时间,开启并行remark: -XX:+CMSParallelRemarkEnabled如果remark还是过长的话,可以开启-XX:+CMSScavengeBeforeRemark选项,强制remark之前开始一次minor gc,减少remark的暂停时间,但是在remark之后也将立即开始又一次minor gc。
5.为了避免Perm区满引起的full gc,建议开启CMS回收Perm区选项:-XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled
6.JDK5默认CMS是在tenured generation沾满68%的时候开始进行CMS收集,JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。如果需要,可以适当调整此值:-XX:CMSInitiatingOccupancyFraction=80
这里修改成80%沾满的时候才开始CMS回收。
7.年轻代的并行收集线程数默认是(cpu <= 8) ? cpu : 3 + ((cpu * 5) / 8),如果你希望降低这个线程数,可以通过-XX:ParallelGCThreads=N 来调整。
7、G1
Garbage First (简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。G1是一款主要面向服务端应用的垃圾收集器。
JDK9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(MinorGC), 要么就是整个老年代(MajorGC), 再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:
传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代

而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。

每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间, 或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX: G1HeapRegionSize设定,取值范围为1MB ~ 32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域 (不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX: MaxGCPauseMilis指定, 默认值是200毫秒),优先处理回收价值最大的那些Region,这也就是"Garbage First"名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
G1的具体工作过程:
G1的GC模式,G1提供了两种GC模式,Young GC和Mixed GC。G1 Young GC它在Eden空间耗尽时会被触发,开始对Eden区进行GC,在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

这时,我们需要考虑一个问题,如果仅仅GC新生代对象,Young区的对象可能还存在Old区的引用, 这就是跨代引用的问题。为了避免对整个堆扫描下来会耗费大量的时间。于是,G1引进了RSet(Remembered Set)和卡表(card table)的概念。基本思想就是用空间换时间。Remembered Set新生代 GC(发生得非常频繁)。一般来说, GC过程是这样的:
首先枚举根节点。根节点有可能在新生代中,也有可能在老年代中。这里由于我们只想收集新生代(换句话说,不想收集老年代),所以没有必要对位于老年代的 GC Roots 做全面的可达性分析。但问题是,确实可能存在位于老年代的某个 GC Root,它引用了新生代的某个对象,这个对象你是不能清除的,G1模式是活对象。关键是怎么快速判断哪些对象是这种对象呢?完全扫描老年代显然不经济。

事实上,对于位于不同年代对象之间的引用关系,虚拟机会在程序运行过程中给记录下来。对应上面所举的例子,“老年代对象引用新生代对象”这种关系,会在引用关系发生时,在新生代边上专门开辟一块空间记录下来,这就是Remembered Set,Remembered Set记录的是新生代的对象被老年代引用的关系。所以“新生代的 GC Roots ” + “ Remembered Set 存储的内容”,才是新生代收集时真正的 GC Roots 。然后就可以以此为据,在新生代上做可达性分析,进行垃圾回收。
G1 收集器使用的是化整为零的思想,把一块大的内存划分成很多个域( Region )。但问题是,难免有一个 Region 中的对象引用另一个 Region 中对象的情况。为了达到可以以 Region 为单位进行垃圾回收的目的, G1 收集器也使用了 Remembered Set 这种技术。G1中每个Region都有一个与之对应的Remembered Set ,在各个 Region 上记录自家的对象被外面对象引用的情况。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set 即可保证不对全堆扫描也不会有遗漏。
G1 GC只在两个场景中依赖RSet:
老年代到年轻代的引用:G1 GC维护了从老年代区间到年轻代区间的指针,这个指针保存在年轻代的RSet里面。
老年代到老年代的引用:从老年代到老年代的指针保存在老年代的RSet里面。
卡表(Card Table)
如果老年代和新生代之间的引用关系很多,要对每个引用都记录在Rset里,Rset会占很大空间,这样得不偿失,为了解决这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。卡表作为一个比特位的集合,每一个比特位可以用来标识老年代某一子区域(这个区域称之为卡。G1是512字节)中的所有的对象是否持有新生代对象的引用,这样新生代GC可以不用花大量的时间扫描老年代对象,来确定每一个对象的引用,而可以先扫描卡表,只有卡表标识为1,才需要扫描该区域的老年代对象,为0则一定不包含新生代的引用。

一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index

G1 Mix GC
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。它的GC步骤分4步:
1、初始标记(initial mark,STW)在此阶段,G1 GC 对根进行标记。
2、并发标记(Concurrent Marking)G1 GC 在整个堆中查找可访问的(存活的)对象。
3、最终标记(Remark,STW)帮助完成标记周期。
4、清除垃圾(Evacuation,STW)识别所有空闲分区;整理堆分区,为混合垃圾回收识别出有高回收价值的老年代分区;RSet梳理。

原始快照(Snapshot At The Beginning,SATB)
提到并发标记,就离不开SATB,说清楚STAB,就要说三色标记算法,三色标记算法是描述追踪式回收器的一种有用的方法,利用它可以推演收集器并发标记的正确性。首先,我们将对象分成三种类型的。
黑色:根对象,或者该对象与它的子对象都被扫描了,确定是活的,灰色:对象本身已被扫描,但还没扫描完该对象中的子对象,也就是对象里的字段属性引用的关系还没扫描完,白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象;当GC开始扫描对象时,按照如下图步骤进行对象的扫描:根对象被置为黑色,子对象被置为灰色。

在GC并发扫描后的结果如下:

在并发标记阶段,应用线程改变了这种引用关系A.c=C

在重新标记阶段扫描结果如下

这种情况下C会被当做垃圾进行回收。Snapshot的存活对象原来是A、B,实际存活对象变成现在A、B、C了,Snapshot的完整遭到破坏了,显然这个做法是不合理。
G1采用的是pre-write barrier解决这个问题。简单说就是在并发标记阶段,当引用关系发生变化的时候,通过pre-write barrier函数会把这种这种变化记录并保存在一个队列里,在JVM源码中这个队列叫satb_mark_queue。在remark阶段会扫描这个队列,通过这种方式,旧的引用所指向的对象就会被标记上,其子孙也会被递归标记上,这样就不会漏标记任何对象,snapshot的完整性也就得到了保证。
CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue ,解决了CMS垃圾收集器重新标记阶段长时间STW的潜在风险。
SATB的方式记录活对象,也就是那一时刻对象snapshot, 但是在之后这里面的对象可能会变成垃圾, 叫做浮动垃圾,这种对象只能等到下一次收集回收掉。在GC过程中新分配的对象都当做是活的,其他不可达的对象就是死的。
如何知道哪些对象是GC开始之后新分配的呢?在Region中通过top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS来记录新配的对象。示意图如下:

其中top是该region的当前分配指针,[bottom, top)是当前该region已用(used)的部分,[top, end)是尚未使用的可分配空间(unused)。
(1): [bottom, prevTAMS): 这部分里的对象第n-1轮concurrent marking已经标记过的对象
(2): [prevTAMS, nextTAMS): 这部分里的对象在第n-1轮concurrent marking是隐式存活的
(3): [nextTAMS, top): 这部分里的对象在第n轮concurrent marking是隐式存活的
清除垃圾(Evacuation)
Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去(并行拷贝),然后回收原本的region的空间。Evacuation阶段可以自由选择任意多个region来独立收集,这些被选中的region构成收集集合(collection set,简称CSet),CSet集合中Region的选定依赖于用户设定允许的收集停顿时间(使用参数-XX: MaxGCPauseMilis指定, 默认值是200毫秒),该阶段并不是选择evacuate所有有活对象的region,只选择回收价值高的少量region来evacuate,这种暂停的开销就可以(在一定范围内)可控。

Full GC
G1的垃圾回收过程是和应用程序并发执行的,当Mixed GC的速度赶不上应用程序申请内存的速度的时候,Mixed G1就会降级到Full GC,使用的是Serial GC。Full GC会导致长时间的STW,应该要尽量避免。
导致G1 Full GC的原因可能有两个:Evacuation的时候没有足够的to-space来存放晋升的对象;并发处理过程完成之前空间耗尽
官方文档
-XX:+UseG1GC
启用G1垃圾收集器
-XX:G1HeapRegionSize=nM(要带单位)
这是G1GC独有的选项,Region的大小默认为堆大小的1/200,也可以设置为1MB、2MB、4MB、8MB、16MB,以及32MB,这六个划分档次。增大Region块的大小有利于处理大对象。前面介绍过,大对象没有按照普通对象方式进行管理和分配空间,如果增大Region块的大小,则一些原本走特殊处理通道的大对象就可以被纳入普通处理通道了。就好比我们在机场安检,飞行员、空姐可以走特殊通道,乘客如果也搞特殊化,一部分人去特殊通道处理,那么特殊通道就得増加几个,相应的普通通道就得减少了,对效率就起了降低作用。反之,如果Region大小设置过小,则会降低G1的灵活性,对于各个年龄代的大小都会造成分配问题。
-XX:MaxGCPauseMillis=200
最长暂停时间设置目标值。默认值是200 毫秒。
-XX:G1NewSizePercent=5
将要使用的堆百分比设置为年轻代大小的最小值。默认值是Java堆的5% 这是一个实验性的标志。有关示例,请参见如何解锁实验性VM标志。要求先配置:-XX:+UnlockExperimentalVMOptions
-XX:G1MaxNewSizePercent=60
将要使用的堆大小百分比设置为年轻代大小的最大值。默认值是Java堆的60%这是一个实验性的标志。要求前面先配置:-XX:+UnlockExperimentalVMOptions
-XX:ParallelGCThreads=n
设置STW工作线程的值。将n的值设置为逻辑处理器的数量。n的值与逻辑处理器的数量相同,最多为8。如果有超过8个逻辑处理器,则将n的值设置为大约5/8个逻辑处理器。这在大多数情况下都是可行的,除了较大的SPARC系统,其中n的值大约是逻辑处理器的5/16。
-XX:ConcGCThreads=n
设置并行标记线程的数目。将n设置为并行垃圾收集线程(ParallelGCThreads)数量的大约1/4。
-XX:InitiatingHeapOccupancyPercent=45
这个选项决定了是否开始一次老年代回收动作,即年轻代GC结束之后,G1会评估剩余的对象是否达到了整个Java堆的45%这个阈值。
-XX:G1MixedGCLiveThresholdPercent=85
在并发标记阶段识别需要被回收的old region,标记成candidate old region,以便在Mixed GC阶段进入CSet而被回收。是通过G1MixedGCLiveThresholdPercent来控制的,当region中的存活数据占比率不超过该阈值时,则表示要被回收,默认占用率为85%。在并发标记阶段每个region中的存活数据占比率会被重新计算,那些存活数据占比较多的region,回收时的代价相对较昂贵,它们还会被标记为expensive region。如果在MixedGC阶段这种region大量进入CSet中可能会导致MixedGC的停顿时间过长。G1为了区分开这些region而做了分开标记,在MixedGC阶段优先回收candidate old region,如果代价许可,会尝试回收expensive region。
-XX:G1HeapWastePercent=5
可容忍的浪费堆空间百分比。如果可回收百分比小于该设置的百分比,JVM不会启动混合垃圾回收周期。
-XX:G1MixedGCCountTarget=8
老年代Region的回收时间通常来说比年轻代Region稍长一些,这个选项可以设置一个并发标记之后启动多少个混合GC,默认值是8个。设置一个比较大的值可以让G1 GC在老年代Region回收时多花一些时间,
如果一个混合GC的停顿时间很长,说明它要做的事情很多,所以可以增大这个值的设置,缩短停顿时间,但是如果这个值过大,也会造成并行循环等待混合GC完成的时间相应的增加。
-XX:G1OldCSetRegionThresholdPercent=10
混合垃圾收集期间,每次能进入CSet的old region的最大阈值(进入CSet表示要垃圾收集)。实验室性标记,默认值是Java堆的 10%。如果该值设置过大,则每次Mixed GC需要收集的old region数量会变多,导致停顿时间拉长。该值可以限制每次Mixed GC最多能回收的old region数量。
-XX:G1ReservePercent=10
选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量; 相对保证晋升对象不会分配失败!
-XX:+UnlockExperimentalVMOptions
要更改实验性标志的值,必须先对其解锁。显式地设置该参数。
当然还有最新的垃圾收集器基于G1改造的,比如RedHat公司发展的Shenandoah,oracle在jdk11推出来的ZGC。在G1、Shenandoah或者ZGC这些越来越复杂、越来越先进的垃圾收集器相继出现的同时,
也有一个“反其道而行”的新垃圾收集器出现在JDK 11的特征清单中一Epsilon, 这是一款以不能够进行垃圾收集为"卖点”的垃圾收集器,"不干活”的收集器。Epsilon收集器由RedHat公司在JEP 318中提出,在此提案里Epsilon被形容成一个无操作的收集器,而事实上只要Java虚拟机能够工作,垃圾收集器便不可能是真正“无操作”的。原因是“垃圾收集器”这个名字并不能形容它全部的职责,更贴切的名字应该是“自动内存管理子系统”。一个垃圾收集器除了垃圾收集这个本职工作之外,它还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责,其中至少堆的管理和对象的分配这部分功能是Java虚拟机能够正常运作的必要支持,是一个最小化功能的垃圾收集器也必须实现的内容。从JDK 10开始,为了隔离垃圾收集器与Java虚拟机解释、编译、监控等子系统的关系,RedHat提出了垃圾收集器的统一接口,即JEP 304提案,Epsilon是这个接口的有效性验证和参考实现,同时也用于需要剥离垃圾收集器影响的性能测试和压力测试。

浙公网安备 33010602011771号