# G1
# G1 简介
G1 将堆划分为不连续空间的(物理内存上不连续) Region,Region 是 G1堆和操作系统交互的最小管理单位。
G1的 Region大致可以分为四类:
- Free Region,没有使用的 Region,之后根据使用情况可能会变成不同类型的 Region
- Eden Region,年轻代的 Region
- Survivor Region,年轻代的 Region
- Old Region 和 Humongous Region,都归属老年代。对象超过 Region 的一半,就认为是大对象,直接分配到 Humongous Region。
一个巨型对象会占据一个或者多个连续 Region (如果没有找到连续 Region 则会进行担保Full GC)。
巨型对象的晋升成本很高,因此对于巨型对象,G1在【年轻代收集时】就会探测其存活性,一旦死亡就会在当次回收释放掉。另外在混合收集周期,因为巨型对象的独占性,巨型对象死亡便是整个分区的死亡,分区就不会再被放入CSet等待回收,而是在清理阶段直接释放掉。
-XX:G1HeapRegionSize
控制一个 Region 的大小,Region 的最大数量是 2048 个。
G1HeapRegionSize 取值为 1-32m,Region Size必须是2的指数幂。
-XX:G1HeapRegionSize=4m
G1还有一个极其重要的特性-软实时(soft real-time):实时回收是指每一次垃圾回收所造成的停顿时间都在限定时间之内。
软实时是指G1允许设置一个限定值,G1会努力控制每一次GC所造成的停顿都在限定时间之内,但是并不保证每一次GC造成的停顿都能满足要求。
停顿时间的限定可以通过 -XX:MaxGCPauseMillis
参数设置,100-500 毫秒都是合理的,默认 200毫秒
如果需要设置此参数,要慎重,过大的话没有意义,过小则会导致频繁触发回收,降低回收效率。
G1 是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间。
G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。
# CSet
CSet 全称 Collection Set。CSet 中保存着每次GC中要回收的Region(年轻代的 Region,Humongous Region,部分回收效率高的老年代)。
回收分区内的存活对象会被复制到新分配的空闲分区中。
因为年轻代是整体回收,所以无论是年轻代收集还是混合收集,所有的年轻代垃圾分区都会被放入CSet。
老年代分区的回收只会在混合收集中涉及,在混合收集过程中,根据收益率将回收收益率较高的候选年老代分区放入CSet等待回收。 G1的分区回收是根据CSet来的,未完全死亡的分区要想被回收,首先要被放入CSet,然后在 Evacuation 阶段,G1会针对CSet中的Region进行回收。 通过 CSet 控制每次回收的分区数量使得G1的回收过程变的可控。
【回收收益率】这个标准通过参数-XX:G1MixedGCLiveThresholdPercent
进行设置,默认为85。意味着一个老年代分区的存活率低于85%才会被列为候选分区。
并不是所有满足【回收收益率】都会被放入CSet,-XX:G1OldCSetRegionThresholdPercent
设置了可以放入CSet的老年代分区数量上限,默认为10,即默认最大有整个堆空间 10% 大小的老年代分区可以被放入CSet中。
# RSet
RSet 全称 Remember Set。
每个区域中都有一个 RSet,每个 Region 的 RSet 都记录着引用当前分区对象的【外部老年代分区对象(只记录外部老年代)】所在的Region_Card。
RSet 通过 hash 表实现,这个 hash 表的 key 是其他 Region 的地址,value是一个数组,数组的元素是引用方的对象所对应的 Card Page 在 Card Table 中的下标。
TIP
b=a,在 A 的 region 中 Rset 有一条记录, key 是 Region B 的地址,value 记录 b 在 card table 中的数组的下标。
RSet 的目的是为了获取在 CSet 中的 Region有没有被不在 CSet 中的 Region 引用,减少堆扫描的时间。
这样在 Evacuation Pauses 阶段对CSet分区进行复制回收时,在新Region中完成 CSet 分区存活对象的复制后,
就可以精准知道该复制对象的引用来自于哪里,从而在复制完成后精确的找了引用方进行引用对象更新。
而不用进行全堆扫描确定引用关系再进行引用地址更新。
为什么 RSet 仅记录来自外部老年代分区的引用呢?
每个 Region 都有一个 RSet,如果全部记录这会比较占内存。为了减少内存的占用,且算法优化,最后是只记录外部老年代的引用。
- 【分区内部有引用关系】,无论是新生代分区还是老生代分区内部的引用,都无需记录引用关系,因为回收的时候是针对一个分区而言,即这个分区要么被回收要么不回收,回收的时候会遍历整个分区,所以无需记录这种额外的引用关系。
- 【新生代分区到新生代分区之间有引用关系】,这个无需记录,原因在于G1的这3中回收算法都会全量处理新生代分区,所以它们都会被遍历,所以无需记录新生代到新生代之间的引用。
- 【新生代分区到老生代分区之间有引用关系】,这个无需记录,对于G1中YGC针对的新生代分区,无需这个引用关系,混合GC发生的时候,G1会使用新生代分区作为根,那么遍历新生代分区的时候自然能找到老生代分区,所以也无需这个引用,对于FGC来说更无需这个引用关系,所有的分区都会被处理。
- 【老生代分区到新生代分区之间有引用关系】,这个需要记录,在YGC的时候有两种根,一个就是栈空间/全局空间变量的引用,另外一个就是老生代分区到新生代分区的引用。
- 【老生代分区到老生代分区之间有引用关系】,这个需要记录,在混合GC的时候可能只有部分分区被回收,所以必须记录引用关系,快速找到哪些对象是活跃的。
一个 Region 要么在 CSet ,要么不在 CSet ,所以来自本分区的引用无需记录。
无论是 young GC 还是混合 GC,新生代总是在 CSet 中,所以来自年轻代分区的引用无需记录也无需记录(年轻代的 Region 都会要扫描)。
RSet 还有一个作用是在年轻代收集时,通过 RSet 确定年老代对年轻代的引用,避免全堆扫描。
# G1 垃圾回收过程
Only Young GC
之后,才会判断是否需要 concurrent marking
, 并发标记之后生成 CSet
G1 有三种回收算法:
- YGC,选定所有的 Eden Region 和 Survivor Region 进行垃圾回收。
年轻代大小会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认整堆60%)之间动态变化。
具体大小由G1根据软实时目标(如果设置的话),回收效率计算得到。
不过G1依然允许通过参数-XX:NewRatio、-Xmn设置固定的新生代大小,但是此时软实时的特性也就失效了。 G1 也不推荐设置固定的年轻代大小。
当年轻代满时(G1算法会动态调整)就会触发年轻代收集,回收年轻分区,并将存活次数达到晋升标准的对象移入老年代。
年轻代分区的收集是 STW 的,用户线程暂停,扫描所有年轻代分区的 RSet,标记出老年代分区引用的对象对根节点进行补充,根据完整的根对象就可以扫描获得了年轻代分区的所有存活对象。
标记完成后 Eden Region 中的存活对象会被复制到新的 Survivor Region;
Survivor Region 的存活对象满足晋升标准的晋升入老年代,不满足晋升标准的依然被复制到新的Survivor Regiong。
之后释放所有的年轻代分区对内存空间进行回收。
此外,如果设置了软实时标准,G1还会根据GC的耗时进行分析,并根据设置的软实时标准调整年轻代尺寸(分区数量),
以此来保证大多的年轻代收集都能满足软实时标准的要求。。
- Mixed GC,选定所有的 Eden Region 和 Survivor Region ,以及部分老年代中的 Region 进行垃圾回收。
随着程序的运行,越来越多的对象晋升入老年代,当老年代堆占比达到 IHOP 阈值(老年代占整堆比,可以通过-XX:InitiatingHeapOccppancyPercent设置,默认45%)时就会触发混合收集,但是混合收集不会立即执行,而是会等待下一次年轻代收集。
另外为了保证停顿时间,混合收集可能只会收集老年代分区的一部分,接着就继续让应用程序运行,并在接下来的连续多次年轻代收集时触发混合收集。
整个连续多次的混合收集过程被称为混合收集周期(Mixed Collection Cycle)。
通过参数 -XX:G1MixedGCCountTarget
控制混合周期中混合收集的最大总次数,默认为8,即整个混合收集周期会进行8次混合收集,每次至少有1/8的候选老年代分区被放入CSet中。
另外通过参数 -XX:G1HeapWasteParcent
来设置混合收集周期的堆废物百分比,默认为5,即当老年代垃圾堆占比低于5%时,即使没达到最大总次数,混合收集周期也会停止,因为过低的垃圾堆占比会使得回收效益变的很低。
- Full GC,整个堆,JDK 10 之前是单线程,之后是多线程收集。
在GC过程中 ,如果无法申请到足够的空闲分区时,G1会首先尝试增加堆空间,如果扩容失败,G1就会触发担保机制:进行一次STW的Full GC,对堆空间进行清除和压缩,只保留存活对象。
回收过程:
初始话标记(Initial Mark,STW):
并发标记(Concurrent Marking)
重标记( Remark,STW)
Cleanup(STW):
Evacuation(STW):将 Region 中存活的对象复制到 Free Region 中。
# G1 JVM 参数设置(默认值指的 JDK 17)
# -XX:+UseG1GC
使用 G1 垃圾收集器。
# -Xms 和 -Xmx
-Xms2g -Xmx2g
一定设置,设置堆最小和最大内存。
当使用 K8s 部署的时候,推荐使用 MaxRAMPercentage 和 MinRAMPercentage 设置堆的大小。
- -Xss
设置线程栈大小,默认 1M(1.8 和 17)
-Xss2m
# -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
元空间的大小,一般256M就足够了,不够再根据实际调整。
# -XX:G1HeapRegionSize=n
-XX:G1HeapRegionSize=2M
设置 G1 中一个 region 多大,单位 M。2 的幂,范围 1-32,可取值:1,2,4,8,16,32
当发现 Humongous Region 较多时,可以考虑增加单个 Region 多大。
# -XX:MaxGCPauseMillis
-XX:MaxGCPauseMillis=200
MaxGCPauseMillis 默认值 200,gc 期望的最大垃圾收集停顿时间 200 ms。
这个会通过动态调整新生代大小,来达到预期最大暂停时间,不一定达到,尽量满足。
# -XX:InitiatingHeapOccupancyPercent
默认值是 45。
-XX:+G1UseAdaptiveIHOP
-XX:InitiatingHeapOccupancyPercent=45
设置老年代占用整个堆达到多少时,触发 Mixed GC。
较低的值可能导致更频繁的混合收集,从而降低吞吐量,但可以减少停顿时间。相反,较高的值可能会减少混合收集的频率,但可能增加停顿时间。
-XX:G1MixedGCLiveThresholdPercent=85
如果一个 Region 中存活对象超过85%,那么该区域将不会被选入到 CSet 进行垃圾回收。
-XX:G1HeapWastePercent=5
当 CSet 中的垃圾对象超过堆的 5%,就会出发 Mixed GC ,低于这个值就不会出发 Mixed GC 。
-XX:G1MixedGCCountTarget=8
Mixed Gc 之后,连续 Mixed Gc 的最多次数。
-XX:G1OldCSetRegionThresholdPercent=10
Mixed GC 时,被选入 CSet 中的 Old Region 的百分比,指的是占用整个堆的百分比。
G1 为了尽可能保证 gc 时间控制在 MaxGCPauseMillis 内,Mixed Gc 的时候,只会选择 G1OldCSetRegionThresholdPercent 内的老年代进行垃圾回收,但是回收之后,垃圾对象还是超过 G1HeapWastePercent ,且在 G1MixedGCCountTarget 内,会继续触发 Mixed Gc。如果条件满足 G1HeapWastePercent 和 G1MixedGCCountTarget 会停止 Mixed Gc。
# -XX:G1ReservePercent(待补充)
G1ReservePercent
参数指定了堆空间预留给 G1 垃圾收集器的一部分内存的百分比。这个预留的内存不会被应用程序直接使用,而是由 G1 垃圾收集器在运行时使用。防止 to-space。
# -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent
G1NewSizePercent 默认:5。
G1MaxNewSizePercent 默认:60。
一般不需要设置。设置的是新生代初始化和最大大小,应为 G1 预测模型会动态调整,一般不修改。
-XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=3
G1年轻代的大小,按照经验一般保证在800M~1G,太大容易导致单次 young GC 的时间太长,太小又增加频率。1g 差不多能保证每次GC在 100ms 的时间。当我们的堆比较大的时候,我们可以调整这个值。
# -XX:GCPauseIntervalMillis
GCPauseIntervalMillis 默认是 MaxGCPauseMillis + 1。
gc 间隔时间,GCPauseIntervalMillis 必须大于 MaxGCPauseMillis。
# -XX:GCTimeRatio = n
-XX:GCTimeRatio=12
GCTimeRatio 用来指定应用程序线程的执行时间与垃圾收集线程执行时间的比率。
这个值影响的是吞吐量。
Jdk 1.8 默认 9。
jdk 17 默认 12。
当为 9 时,意味着 gc 执行时间占 1/(9+1)
=10% 。
当为 12 时,意味着 gc 执行时间占 1/(12+1)
= 7.69% 。
增大这个值,意味着 gc 占用时间减少,由于垃圾收集所占用的时间相对减少,垃圾收集的频率可能会降低。这意味着每次垃圾收集的停顿时间可能会更长,但是在整体上,应用程序的运行时间可能会更长。
服务端我们一般追求的是低延迟,所以我们更关注的是 MaxGCPauseMillis。
# -XX:ParallelGCThreads
初始化标记阶段和重新标记阶段,这个阶段 STW,只有 GC 线程执行,设置并行 GC 线程的数量。
在未明确指定的情况下,JVM会根据 **逻辑核数 ** ncpus,采用以下公式来计算默认值:
- 当 ncpus 小于8时,ParallelGCThreads = ncpus
- 否则 ParallelGCThreads = 8 + (ncpus - 8 ) ( 5/8 )
# -XX:ConcGCThreads
并发标记阶段,gc 运行的线程数量。默认为 ParallelGCThreads 的 1/4 。这个时候,GC 线程和用户应用线程一起工作,这个值,不要太大。否则影响用户线程执行。
# -XX:GCCardSizeInBytes
如果垃圾收集的时间,60 以上在 【High Merge Heap Roots】 and 【Scan Heap Roots Times】这两个阶段,减少 Card 的大小,减少扫描空间。
# 参考文档
Getting Started with the G1 Garbage Collector (opens new window)