大纲
1.G1的YGC过程
2.YGC并行处理阶段的过程
3.YGC串行处理阶段的过程(一)
4.YGC串行处理阶段的过程(二)
5.整个YGC的执行流程总结
1.G1的YGC过程
(1)YGC相关的一些参数
(2)YGC和MixedGC、FGC之间的关系
(3)YGC使用的算法 + 新生代的垃圾回收流程
(1)YGC相关的一些参数
一.-XX:+UseG1GC
设置使用G1垃圾回收器。
二.-XX:G1HeapRegionSize
设置Region分区大小,最小值1M,最大值32M,且只能是2的n次幂。
三.-Xms和-Xmx或者InitialHeapSize和MaxHeapSize
设置堆内存最大值最小值。
四.-XX:NewSize和-XX:MaxNewSize
设置新生代最小值和最大值。注意,在G1里这个最大值最小值其实是可以不设置的。G1会自动计算出一个值:从5%的Region数量开始,慢慢增加到最大为60%的Region数量。一般不用指定新生代的最大值和最小值,按照默认的5%~60%即可。
五.新生代Region数量
下限-XX:G1NewSizeRercent,默认5%
上限-XX:G1MaxNewSizePercent,默认60%
六.新生代Eden和Survivor的比例:-XX:SurvivorRatio=n
默认为8,即eden : s1 : s2 = 8 : 1 : 1。这个比例和ParNew的原理是一致的,比如总的新生代是100个Region,那么Eden区有80个,两个S区各有10个。
七.-XX:MaxGCPauseMills=n
设置最大GC暂停时间,这是一个大概值,JVM会尽可能的满足此值。例如设置200ms,那么G1就会在每次GC时努力保证GC的停顿时间在这个范围内。
八.-XX:NewRatio=n
新生代与老年代的大小比例,默认值2。只设置一个NewRatio,与只设置一个Xmn是相当的。最好不要设置一个Xmn,或者最好不要单独设置一个NewRatio。因为这样会固定新生代大小,不利于停顿时间预测。
九.-XX:ParallelGCThreads=n
参与回收的线程数量,默认和CPU核数相等。
(2)YGC和Mixed GC、FGC之间的关系
G1中的YGC、Mixed GC以及FGC和ParNew + CMS是有相似之处的。比如,在新生代GC后,会有存活对象进入老年代。如果老年代对象占用达到了某个阈值,就会触发老年代的回收。在ParNew + CMS中,是直接触发FGC,而在G1中是触发Mixed GC。
在G1中首先会进行YGC,YGC会选择所有新生代的分区进行回收。当程序不断运行,存活对象越来越多,老年代的对象越来越多时,就会在某次YGC的时候,触发一个并发标记过程。
然后等待YGC和这次并发标记过程结束后,就会正式进入Mixed GC。Mixed GC会从老年代中选择部分回收价值比较高的Region进行回收,从而满足用户设置的MaxGCPauseMills值,当然Mixed GC也会回收所有新生代分区。当Mixed GC后,对象还是无法分配成功时,就会触发FGC。FGC会暂停程序运行,对整个堆进行全面的垃圾回收。FGC的回收会包括新生代、老年代、大对象等。
YGC、Mixed GC、FGC间的过程转换关系如下:
(3)YGC使用的算法 + 新生代的垃圾回收流程
一.不均匀的分区分布
二.对象在Eden区的分布
三.Eden区占满时触发YGC
四.标记存活对象
五.复制存活对象到Survivor区
六.回收垃圾对象
七.动态调整新生代区域Region数量
八.是否需要开启并发标记
九.新生代的垃圾回收流程结束
YGC使用的算法是复制算法,也就是会把新生代的所有Region按照Eden、Survivor做类型标识。在执行垃圾回收时,首先对存活对象进行标记。然后把存活对象复制到S区,接着就把所有的垃圾对象全部回收掉。注意:G1的Region分布,对于一个分代而言,不一定是连续的。
一.不均匀的分区分布
二.对象在Eden区的分布
三.Eden区占满时触发YGC
四.标记存活对象
首先从GC Roots出发,标记直接引用的对象,然后再一个一个标记GC Roots间接引用的对象。
五.复制存活对象到Survivor区
六.回收垃圾对象
如果是ParNew + CMS的新生代回收,其实到这里就基本上是结束了。
七.动态调整新生代区域Region数量
YGC回收掉的那些Region,是有可能会成为自由分区的。因为YGC回收后会动态判断,如果需要更多的Region就进行增加。如果回收时间太长,发现YGC下一次可能没有那么强的能力,那么G1就会减少几个Region。
八.是否需要开启并发标记
如果老年代的使用率达到了阈值,就开启并发标记。
九.新生代的垃圾回收流程结束
以上就是G1新生代垃圾回收的基本流程。
(4)总结
G1的YGC过程:
一.YGC相关的一些参数
二.YGC和MixedGC、FGC是什么关系
三.YGC使用的算法 + 新生代的垃圾回收流程
1.不均匀的分区分布
2.对象在Eden区的分布
3.Eden区占满时触发YGC
4.标记存活对象
5.复制存活对象到Survivor区
6.回收垃圾对象
7.动态调整新生代区域Region数量
8.是否需要开启并发标记
9.新生代的垃圾回收流程结束
2.YGC并行处理阶段的过程
(1)YGC的并行处理是什么
(2)GC Roots并行标记及RSet并行更新
(3)YGC并行标记阶段不仅会标记还会复制
(4)完成初始标记后的处理------将直接引用的对象的字段入栈
(5)遍历栈中的字段找出存活对象并复制到S区然后清理对象 + 清空栈
YGC有并行处理的过程以及串行处理的过程。
(1)YGC的并行处理是什么
YGC的过程,肯定是要做一些并行化处理的,否则速度就会比较慢。比如,标记对象时就不能一个一个对象去查找标记。所以,会有线程对GC Roots直接引用的对象进行标记。
(2)GC Roots并行标记及RSet并行更新
不仅在对GC Roots引用的对象进行标记时,会使用并行处理的方式。在对RSet进行更新时,也会用并行处理的方式。
G1在进行YGC时:会从RSet和GC Roots出发遍历所有新生代对象,然后标记存活对象。由于RSet的更新不一定会在YGC前就更新完毕,所以在YGC并行处理这个阶段,还要对RSet做并行处理的更新。即把DCQS里还没处理完毕的跨代引用关系变更,更新到RSet里面。
更新RSet完成后,再从RSet出发,去标记被RSet指向的老年代空间里的对象直接引用的新生代对象。
整个标记GC Roots + 更新RSet的过程,是由多个线程一起并行处理的。比如现在有4个GC线程参与垃圾回收,那么就会有两个线程从GC Roots出发去标记对象,有两个线程去消费DCQS然后更新RSet。接着Rset更新完毕后,就把RSet作为GC Roots继续去执行对象标记工作。
(3)YGC并行标记阶段不仅会标记还会复制
在YGC的并行标记阶段,不仅仅会根据GC Roots + RSet来追踪所有直接引用的对象。由于在执行YGC的过程中,复制操作和标记操作是同时进行的。所以在用GC Roots标记直接引用的存活对象时,也会进行复制操作。比如发现4个对象是由GC Roots直接引用的。
此时通过GC Roots找到这4个对象后,就会复制它们到一个Survivor区。
所以YGC里的复制算法,并不是等待全部标记完成,再去复制对象。而是找到一个直接引用的存活对象,就会复制到Survivor里了。
另外,把RSet作为GC Roots的意思是:RSet中映射到卡表对应的卡页中的所有对象都会作为GC Roots。因为卡页本身很小,对象数量也很少,所以可以把RSet都作为GC Roots。然后找到这些GC Roots直接引用的对象,再复制到Survivor区。
(4)完成初始标记后的处理------将直接引用的对象的字段入栈
仅仅处理这些GC Roots直接引用的对象还是不够的,因为还有很多对象会被它们间接引用。间接引用的对象,也需要全部找到并进行标记。
那么在并行处理阶段,GC线程还需要做的另外一件事就是:把刚刚找到的被GC Roots直接引用的哪些对象的字段Field,全部都给放入一个栈里面。
为什么要这么做?因为要把这些对象引用的所有对象都找到才行,找到它们引用的对象才能找到所有存活的对象。所以在把GC Roots + RSet直接引用的对象复制到S区时,就会把它们的所有字段放入一个栈中。
(5)遍历栈中的字段找出存活对象并复制到S区然后清理对象 + 清空栈
等到所有的GC Roots + RSet直接引用的对象都复制完毕后,再逐一对栈中的字段Feild进行遍历,找到所有存活的对象,然后再把找到的存活对象放入Survivor区中。
最后一口气回收掉所有的垃圾对象。
此时栈就会被清空掉了。
至此,YGC的并行操作基本已经结束,基本上YGC其实已经结束了。但是,实际上还会有后续的很多操作。比如以下操作就是在YGC的串行执行过程中需要做的,需要更新RSet、RSet卡表、释放被回收垃圾占用的Region、动态调整新生代分区数量来实现停顿预测模型等。
(6)总结
YGC并行处理阶段的过程:
一.YGC的并行处理是什么
二.GC Roots并行标记及RSet并行更新
三.YGC并行标记阶段不仅会标记还会复制
四.完成初始标记后的处理------将直接引用的对象的字段入栈
YGC的并行处理阶段具体会做的事情:
一.并行更新RSet
二.将更新完RSet加入GC Roots进行并行标记
三.并行复制直接引用的存活对象进入S区
四.将直接引用的存活对象的所有字段入栈
五.遍历栈的所有字段寻找所有存活对象
六.复制所有存活对象进入S区
七.清空全部垃圾对象
八.清空栈里的所有字段
3.YGC串行处理阶段的过程(一)
(1)YGC中的串行处理是什么
(2)YGC中的串行处理操作有哪些
一.软引用、弱引用、虚引用的处理
二.整理卡表
三.Redirty操作------清理旧RSet建立新RSet
四.释放分区
(1)YGC中的串行处理是什么
所谓串行处理,就是要一步步操作,否则就可能会出现错乱的一些操作。JVM会对垃圾回收中的一些操作使用串行化的处理方式。可能因为这些操作会有前后影响、或消耗的时间很少,所以才用串行化。当然不排除JVM后面可能会把这些操作优化成并行化的处理方式。
其中G1中的GC Roots追踪、RSet更新,这两个操作是可以并行进行的,因为这两个操作基本上不会出现互相影响的情况。但是YGC的其他一些操作,是有可能会出现先后影响的。
(2)YGC中的串行处理操作有哪些
一.软引用、弱引用、虚引用的处理
该操作是把这些引用中使用的存活对象也复制到新分区,否则就会出错。YGC中的并发处理阶段针对的是强引用对象。
软引用的回收时机:在第一次FGC时,是不回收软引用的。只有在第二次Full GC时,才会回收软引用。
在YGC执行串行处理操作时:就会把新生代里被这类引用给引用到的对象复制到Survivor区中。
二.整理卡表
卡表是一个全局卡表。在新生代在回收后,有些对象已被回收清除了,有些对象已经换了位置。这时就要把卡表中这些对象的描述数据也给清除掉和更新掉。
因为卡表中是一个字节描述512字节的内存空间。如果某内存空间被清除了,那么卡表的描述数据也需清空,否则会出错。
所以整理卡表的操作就是把已清理过的Region对应的卡表进行清空,同时把对象复制后所在的Region对应的卡表也进行修改,从而保证卡表中的描述数据是正确的。整理卡表的这个过程是很快的。
三.Redirty操作------清理旧RSet建立新RSet
这一步的主要操作,其实就是重构一下Rset。在做完垃圾回收后,新生代对象因为复制,其地址已经发生变化了。那么老年代引用的新生代对象所在Region的RSet此时还没有修改,因此需要把这个旧的RSet进行清理,然后建立一个新生代对象所在的新Region的RSet。重构RSet的过程也是很快的。
四.释放分区
新生代GC,需要把所有的非Survivor区的新生代Region都给清理掉。此时它们还被标记为Eden,或者Survivor(原本就可能有一些垃圾对象)。清理后,需要把这些Region分区给释放掉,否则需要分区时可能不够用。释放分区就是清空这些分区的标记,然后把清空后的分区加入到自由分区列表。
(3)总结
YGC串行处理阶段的过程:
一.YGC中的串行处理是什么
二.YGC中的串行处理操作有哪些
1.软引用、弱引用、虚引用的处理
2.整理卡表
3.Redirty操作------清理旧RSet建立新RSet
4.释放分区
(4)问题
如果我们是G1的开发者,在上面的流程结束之后还需要做什么?G1本身的设计思路就是,要垃圾回收优先,要满足系统的停顿时间。那么在GC之后最重要的事情是什么呢?
一.停顿预测模型和Region数量分配有关;
二.RSet处理时是比较耗时的,GC开启时Refine线程就会暂停,由GC线程来继续执行后续的操作。那么对于这个RSet、DCQ、DCQS的处理,是否需要调整?
三.是否需要扩展内存?
4.YGC串行处理阶段的过程(二)
(1)尝试对大对象进行回收(性价比很高)
(2)尝试扩展内存
(3)调整新生代分区的数目及Refine线程阈值
(4)尝试启动并发标记
(1)尝试对大对象进行回收(性价比很高)
一.为什么要对大对象尝试进行回收操作
原因一:大对象本身占用很多空间的,最少也会占1/2的Region
假如大对象能够回收,就顺带把它回收掉,这样就能腾出一块非常可观的空间出来了。
原因二:大对象回收起来不麻烦
因为大对象创建时是单独存储在一个分区(多个分区)的,属于单独存储。
二.如何判断大对象是否存活
由于每个Region都维护了一个RSet,并且RSet里存储的是引用关系信息。那么在YGC的串行处理阶段查看大对象所在Region的RSet,就能知道是否有其他对象在引用了。
注意大对象所在的Region的RSet不会有很多内容,最多就是两个对象被引用的关系。
所以如果大对象没有横跨多个分区,则只需判断一下大对象所在的Region的RSet里是否有内容,就可以判断大对象是否存活了。
如果大对象横跨多个分区,那么直接判断大对象所在的第一个Region的RSet里是否有内容,就可以知道大对象是否被引用了。
通过简单的判断就可能回收大量的空间,性价比非常高。所以要在YGC的串行处理阶段尝试一下对大对象进行回收。
(2)尝试扩展内存
前面介绍新生代内存时,介绍过可能会对新生代内存进行扩展,在YGC的串行处理阶段尝试扩展内存就是扩展新生代内存的时机之一。
完成YGC后会统计一下执行这次YGC的花费时间,而且还会统计一下在执行YGC前的系统运行总时间。于是就可以判断,这次YGC执行时间和系统运行总时间的比例是否合理。如果不合理,就要考虑扩展一下新生代内存,如果合理就没必要扩展了。
对应的参数是:GCTimeRatio和G1ExpandByPercentOfAvailable。其中GCTimeRatio是指:程序运行时间与YGC时间的比例。如果YGC时间占程序运行时间比例超过10%,就说明要扩展新生代内存。
为什么YGC时间占程序运行时间的比例超过10%就要扩展新生代内存?因为YGC时间占比超过10%,就说明要么YGC频繁、要么YGC时间太长。如果新生代空间足够大,加上G1会自己动态调整新生代分区的数量,那么就是YGC太频繁导致YGC的时间占程序运行时间比例超过10%。
YGC过于频繁,必然会导致判断出大量对象存活,相当于变相拖慢YGC。YGC中真正耗时的不是清理大量垃圾对象的过程,而是进行标记的过程。YGC中存活对象越多,进行标记的过程就越长。YGC越频繁 -> 说明新生代很快满了 -> 说明新生代需要扩展内存
如果YGC时间占程序运行时间的比例没有超过10%,则暂时不需要扩展,扩展的内存大小和G1ExpandByPercentOfAvailable有关。
(3)调整新生代分区的数目及Refine线程阈值
一.调整新生代分区数目
这个是YGC串行处理阶段的一个重点,因为对于G1来说,控制停顿时间是非常重要的。
要想控制好停顿时间:只能在系统运行时间和YGC过程中各个步骤的耗时上进行综合考量。综合考量后还要进行动态调整,这样才能保证停顿时间是可以被满足的。
那么在YGC后,首先就需要判断一下:现在的YGC耗时、YGC能力能否让下一次GC满足预期停顿时间。如果不能的话,那么就需要把新生代分区减少一些,不然就满足不了了。如果远远没达到停顿预测时间的阈值,那么就可以增加一些新生代分区。
所以这一步,就会根据当前YGC的执行时间和目标停顿时间,进行预测。看下一次YGC最多能回收多少分区,然后和当前新生代的总分区数对比。如果下次最多能回收的分区和当前新生代总分区数差不多,则无需调整。如果发现预测出来下一次YGC能回收1000个分区,而现在才600个分区。那么就可以多增加几个分区到新生代里,避免浪费堆内存。
二.调整RefinementZone的阈值
关于DCQS、DCQ和Refine线程的处理:如果DCQ比较多,则需要启动多个Refine线程去进行处理。而且在YGC开始时,这些Refine线程就会暂停,并且由YGC线程接管其工作来处理后续的DCQ。
如果YGC线程处理DCQ的时间过多,那么代表了什么?代表Refine线程的数量,或者DCQS的四个区域设置得不合理。如果设置合理,Refine线程在对应的区域中,就可满足DCQ消息的处理。此时YGC线程最多就是进行少部分的收尾工作,但现在YGC线程还需要大量的时间去处理DCQ消息,那么就说明:要么这几个DCQS的阈值设置得过大了、要么Refine线程太少了。
Refine线程理论上是不能在GC过程中动态调整上限的,所以我们只能调整DCQS的白绿黄红几个阈值的大小,通过白绿黄红来匹配Refine线程的处理能力。
比如把DCQS的各个阈值给降下来,然后把总长度也降下来。让系统线程也帮忙处理DCQ,这样就可以让GC线程的压力小一点。所以如果YGC处理DCQ时间过长,会导致DCQS的长度和阈值动态减小。
(4)尝试启动并发标记
这个过程也是一个尝试的过程。因为新生代的回收是一直在进行的,老年代的对象也是一直在累积的。
如果老年代对象累积到一定程度,那么此时就需要回收一部分老年代的垃圾对象,否则内存使用率就会太高。所以,在老年代达到45%的内存使用率时,一次YGC结束后就会开启一个并发标记过程。
如下图示:老年代占用内存达到阈值的判断,就是判断是否要进入MGC + YGC过程。如果成功启动了并发标记,就意味着接下来要进入Mixed GC了。
5.整个YGC的执行流程总结
YGC算法的流转过程: