JVM实战—G1垃圾回收器的原理和调优

1.G1垃圾回收器的工作原理

(1)ParNew + CMS的组合有哪些痛点

Stop the World是最大的问题。无论是新生代GC还是老年代GC,都会或多或少产生STW现象,这对系统的运行是有一定影响的。

所以JVM对垃圾回收器的优化,都是朝减少STW的目标去做的。在这个基础之上,就诞生了G1垃圾回收器。G1垃圾回收器可以提供比ParNew + CMS组合更好的垃圾回收性能。

(2)G1垃圾回收器

G1垃圾回收器可以同时回收新生代和老年代的对象,不需要两个垃圾回收器配合起来运作,它自己就能搞定所有的垃圾回收。G1的一大特点就是把Java堆内存拆分为多个大小相等的Region。如下图示:

然后G1也会有新生代和老年代,但是只是逻辑上的概念。也就是说,某些Region属于新生代,某些Reigon属于老年代。如下图示:

G1的另一特点,就是可以设置每次垃圾回收时的最大停顿时间,以及指定在一个长度为M毫秒的时间片段内,垃圾回收时间不超N毫秒。

比如可指定,希望G1在垃圾回收时保证:在1小时内由G1垃圾回收导致系统停顿时间,不超过1分钟。

从前面的JVM优化思路可知,我们对内存合理分配,优化一些参数,就是为了尽可能减少YGC和FGC,尽量减少GC带来的系统停顿影响。

现在G1则可以直接指定在一个时间段内,垃圾回收导致的系统停顿时间不能超过多久。而G1会全权进行负责,保证达到这个目标,这样就相当于我们可以控制垃圾回收对系统性能的影响了。

(3)G1如何实现垃圾回收的停顿时间是可控的

如果G1要做到这一点,就必须要追踪每个Region里的回收价值。

什么是回收价值?即G1必须搞清楚每个Region里有多少垃圾对象。如果对一个Region进行垃圾回收,会耗费多长时间,可回收多少垃圾?

如下图示:G1通过追踪发现,1个Region中的垃圾对象有10M,回收它们要耗费1秒。另外一个Region中的垃圾对象有20M,回收他们需要耗费200毫秒。

然后在GC时G1发现在最近一个时间段内,垃圾回收已导致几百毫秒的系统停顿。现在又要执行一次垃圾回收,那么对这些Region进行筛选后,发现必须回收上图中只需200ms就能回收20M的Region。如下图示:

所以G1的核心设计是:G1可以让我们设定垃圾回收对系统的影响,G1会把内存拆分为大量的小Region,G1会追踪每个Region中可以回收的对象大小和预估时间,G1在垃圾回收时会尽量把垃圾回收对系统影响控制在指定时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象。

(4)Region可能属于新生代也可能属于老年代

在G1中,每一个Region可能属于新生代,也可能属于老年代。刚开始一个Region可能谁都不属于,然后接着就被分配给了新生代。然后这个Region会被放入很多属于新生代的对象,接着触发了垃圾回收,需要回收这个Region。如下图示:

然后下一次这个Region可能又被分配给了老年代,用来存放老年代需要长期存活的的对象。如下图示:

所以在G1的内存模型中,一个Region会属于新生代也会属于老年代。于是就没有所谓新生代给多少内存,老年代给多少内存这一说法。新生代和老年代各自的内存区域是不停变动的,由G1自己去控制。

(5)总结

这里介绍了G1垃圾回收器的设计思想:包括Region划分、Region动态变成新生代或老年代,Region的按需分配。当触发G1垃圾回收时,可以根据设定的预期的系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收。保证GC对系统停顿的影响在可控范围内,同时尽可能回收最多对象。

接下来会介绍关于G1的更多技术细节,比如:

一.G1是如何工作的

二.对象什么时候进入新生代的Region

三.什么时候触发Region GC

四.什么时候对象进入老年代的Region

五.什么时候触发老年代的Region GC

2.G1分代回收原理---性能为何比传统GC好

(1)G1垃圾回收器的设计思想

G1垃圾回收器设计的思想:就是把内存拆分为很多Region,然后新生代和老年代各自对应一些Region。回收的时候尽可能挑选停顿时间最短以及回收对象最多的Region,从而尽量保证达到指定的垃圾回收系统停顿时间。

(2)如何设定G1对应的内存大小

一.G1会把内存拆分为很多个Region内存区域,每个Region大小都一样

如下图示:

二.每个Region的大小范围是1M~32M,而且必须是2的倍数

通过-Xms和-Xmx参数可以设置整个堆内存的大小,通过-XX:+UseG1GC参数可以指定使用G1垃圾回收器。

如果JVM启动时发现了指定使用G1垃圾回收器,那么默认情况下G1会自动用堆大小除以2048得出每个Region的大小。每个Region的大小范围是1M~32M,且必须是2的倍数。如果堆大小是4G = 4096M,除以2048,每个Region的大小就是2M。当然也可以通过-XX:G1HeapRegionSize参数来手动指定Region大小。

需要注意的是:按照默认值计算,G1可以管理的最大内存为2048 * 32M = 64G。假设设置xms=32G,xmx=128G。由于Region的大小最小是1M,最大是32M,而且要是2的倍数。那么初始化时按2048个Region计算,得出每个Region分区大小为32M。然后分区个数动态变化范围从1024个到4096个。

系统刚开始运行时,默认新生代对堆内存的占比是5%。也就是占据200M左右的内存,对应大概是100个Region。这可以通过-XX:G1NewSizePercent来设置新生代初始占比,但通常维持默认值即可。

因为在系统运行中,JVM会不停地给新生代增加更多的Region。但新生代占比最多不超60%,可通过-XX:G1MaxNewSizePercent设置。而且一旦Region进行了垃圾回收,新生代的Region数量就会减少。

如下图示,系统刚开始运行时有一部分的Region是属于新生代的。

(3)新生代Region还会分Eden区和Survivor区

G1虽然把内存划分为很多的Region,但还是有新生代、老年代的区分,而且新生代里同样有Eden和Survivor的划分。

所以前面介绍的很多原理在G1中都还是适用的。比如参数-XX:SurvivorRatio=8,系统刚开始运行时有100个Region。此时新生代中有80个Region是Eden区,20个Region是两个Survivor区。如下图示:

所以在G1中还是有Eden和Survivor的,它们会占据不同数量的Region。然后随着对象不停地在新生代分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加。

(4)G1的新生代垃圾回收

既然G1的新生代有Eden和Survivor之分,那么垃圾回收的机制也类似。当不停往新生代Eden的Region放对象,G1会不停给新生代加入Region。直到新生代占据堆大小的最大比例60%,一旦新生代大小达到了设定的占据堆内存大小的最大比例60%。比如2048个Region中有1200个Region都是属于新生代的了,里面的Eden占了1000个Region,每个Survivor占了100个Region,而且Eden中的Region都占满了对象。如下图示:

这时就会触发新生代GC。G1就会使用复制算法来进行垃圾回收,进入Stop the World状态。然后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden对应的Region中的垃圾对象,如下:

G1的新生代垃圾回收过程和ParNew是有区别的。因为G1可以设定GC停顿时间,执行GC时最多会让系统停顿某个时间。可以通过-XX:MaxGCPauseMills参数来设定,默认值是200ms。G1会追踪每个Region,然后GC时根据回收各Region需要多少时间、以及可回收多少对象,来选择回收其中一部分Region。从而保证GC时的停顿时间控制在指定范围内,并尽可能多地去回收对象。

(5)对象什么时候进入老年代

在G1的内存模型下,新生代和老年代各自都会占据一定的Region。如果按照默认新生代最多只能占据堆内存2048个Region的60%的Region来推算,老年代最多可以占据40%的Region,大概就是800个左右的Region。

那么对象何时候会从新生代进入老年代?和ParNew几乎一样,还是以下几个条件:

一.对象在新生代躲过多次YGC,达到参数-XX:MaxTenuringThreshold设置的年龄

二.动态年龄判定规则,比如年龄为1岁、2岁、3岁、4岁的对象大小总和超过了Survivor的50%,此时Survivor区还有5岁+的对象,那么4岁及以上的对象就会全部进入老年代

三.新生代回收后存活的对象在Survivor区的Region都放不下了

所以经过一段时间的新生代使用和垃圾回收后,会有些对象进入老年代。如下图示:

(6)大对象Region

一.G1内存模型下对大对象的分配策略

G1提供专门的Region存放大对象,不让大对象进入老年代的Region。G1中大对象的判定规则就是一个大对象超过了一个Region大小的50%。比如按照上面算的,每个Region是2M。那么只要一个大对象超过了1M,就会被放入大对象专门的Region中,而且一个大对象如果太大,可能会横跨多个Region来存放。如下图示:

堆内存里哪些Region会用来存放大对象?60%的Region给新生代,40%的Region给老年代,那还有哪些Region给大对象?

其实在G1里,新生代和老年代的Region是不停的动态变化的。比如新生代现占1200个Region,但一次GC后里面1000个Region空了。此时这1000个Region就可以不属于新生代,可用部分Region放大对象,所以大对象既不属于新生代也不属于老年代。

二.G1内存模型下对大对象的回收策略

既然大对象既不属于新生代也不属于老年代,那何时会触发垃圾回收?

其实在新生代、老年代回收时,会顺带着大对象Region一起回收,这其实就是在G1内存模型下对大对象的分配和回收策略。

(7)总结

这里介绍了G1的内存模型和分配规则,包括:

一.每个Region多大(1-32M)

二.新生代包含多少Region(60%)

三.新生代动态增加Region(初始5% -> 60%)

四.G1中仍然存在Eden和Survivor两个区域

五.什么时候触发新生代的垃圾回收(新生代达到60%占比且满了)

六.G1新生代垃圾回收使用的复制算法

七.G1特有的预设GC停顿时间功能

八.对象进入老年代(15岁 + 动态年龄 + S区不足)

九.大对象的独立Region存放和回收

(8)问题

从新生代的垃圾回收来看,G1相比ParNew的优点:

一.停顿时间可以预设

二.大对象不再进入老年代

三.对象进入老年代的情况少很多

四.同样内存大小,Eden和Survivor都大很多

五.ParNew的GC需要停止系统程序,但G1的新生代GC可以不用停止

3.使用G1垃圾回收器时应如何设置参数

(1)G1的动态内存管理策略总结

G1的动态内存管理策略:根据情况动态地把Region分配给新生代(Eden+S区)、老年代和大对象。但是新生代和老年代会有一个各自的最大占比,新生代占比最大60%,老年代占比最大40%。然后在新生代的Eden满的时候,触发新生代垃圾回收。

G1新生代的垃圾回收还是采用了复制算法。只是会考虑预设GC停顿时间,保证垃圾回收的停顿时间不超预设时间。因此会挑选一些回收价值比较高的Region来进行垃圾回收。

然后G1新生代垃圾回收和ParNew一样:如果一些对象在新生代熬过一定次数GC,或触发了动态年龄判定规则,或GC后的存活对象在Survivor放不下,都会让对象进入老年代中。所以G1中的新生代对象还是会因为各种情况而慢慢地进入老年代的。

G1对大对象的处理则与ParNew不一样:G1的大对象会进入单独的大对象Region,不再进入老年代。

(2)何时触发新生代 + 老年代的混合垃圾回收

-XX:InitiatingHeapOccupancyPercent是G1的参数,默认值是45%。意思是如果老年代占据了堆内存的45%的Region时,就会尝试触发新生代 + 老年代一起回收的混合回收。

比如按照默认情况下的堆内存有2048个Region:如果老年代占据了其中45%的Region,就会开始触发混合回收。如下图示:

(3)G1混合垃圾回收的过程

G1:初始标记-并发标记-最终标记-混合回收

CMS:初始标记-并发标记-重新标记-并发清除

一.首先进入初始标记阶段

这个阶段需要STW,标记GC Roots直接引用的对象,这个过程速度是很快的。

如下图示:首先STW停止系统程序的运行。然后对各个线程栈内存中局部变量所代表的GC Roots,以及方法区中类静态变量所代表的GC Roots,进行扫描。也就是标记出这些GC Roots直接引用的对象。

二.然后会进入并发标记阶段

这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,如下:

这里对GC Roots追踪进行说明,代码如下:

public class Kafka {
    public static ReplicaManager replicaManager = new ReplicaManager();
}
public class ReplicaManager {
    public ReplicaFetcher replicaFetcher = new ReplicaFetcher();
}

可以看到:Kafka类有一个静态变量是replicaManager,它就是一个GC Root对象。首先在初始标记阶段,仅仅会标记GC Roots直接引用的对象。所以会标记replicaManager作为GC Roots直接关联的对象,也就是表明堆内存中的ReplicaManager对象,它肯定是要存活的。

然后在并发标记阶段,就会进行GC Roots追踪。即会从replicaManager直接关联的ReplicaManager对象开始往下追踪,ReplicasManager对象里有一个实例变量replicaFetcher,此时追踪这个replicaFetcher变量可知它引用了ReplicaFetcher对象,于是这个ReplicaFetcher对象也要被标记为存活对象。

这个并发标记阶段还是很耗时的,因为要追踪全部的存活对象。但这个阶段可以跟系统程序并发运行,所以对系统程序影响不太大。而且在并发标记阶段对对象进行的修改,JVM也会记录起来。比如哪个对象被新建了,哪个对象失去了引用。

三.接着会进入最终标记阶段

这个阶段会STW禁止系统程序运行,但会根据并发标记时的记录,最终标记出哪些对象存活、哪些对象回收。如下图示:

四.最后进入混合回收阶段

这个阶段首先会进行如下计算:老年代中各Region的存活对象数量、存活对象占比,还有执行垃圾回收的预期性能和效率。

接着会Stop The World停止系统程序,选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在指定的范围内。

比如老年代此时有1000个Region都满了:但是根据预定目标,本次垃圾回收可能只能停顿200毫秒。那么通过之前计算得知,可能回收其中800个Region刚好需要200ms。于是就只回收那800个Region,把GC停顿时间控制在指定范围内。如下图示:

需要注意的是:老年代对堆内存占比达到45%时,触发的是混合回收。此时垃圾回收不仅会回收老年代,还会回收新生代,还会回收大对象。

那么到底会回收这些区域的哪些Region,那就要看情况了。因为G1为了满足设定的GC停顿时间要求,会从新生代、老年代、大对象里各自挑选一些Region,保证在指定的时间范围内(比如200ms)回收尽可能多的垃圾对象。这也就是所谓的混合回收,如下图示:

(4)G1垃圾回收器的一些参数

在老年代的Region占据堆内存Region的45%之后,会触发混合回收。混合回收也就是Mixed GC,进行混合回收时会分为如下四个阶段:初始标记 -> 并发标记 -> 最终标记 -> 混合回收。在最后的混合回收阶段,会从新生代和老年代中都回收一些Region。

注意:G1会执行多次混合回收。即G1在最后的混合回收阶段时,会多次停止运行的系统程序。比如先停止系统运行,执行一次混合回收。回收掉一些Region后,恢复系统运行。然后再次停止系统运行,接着又执行一次混合回收。回收掉一些Region,恢复系统运行。

一.-XX:G1MixedGCCountTarget指定混合回收阶段会执行多少次回收

在一次MixedGC过程中,最后一个阶段应执行多少次回收,默认8次。为什么在最后一个混合回收阶段需要反复回收多次呢?因为停止系统一会儿,回收掉一些Region,再让系统运行一会儿。然后再次停止系统一会儿,再次回收掉一些Region。这样可以尽可能让系统的停顿时间不会太长,可以在多次回收的间隙,也运行一下程序。

二.-XX:G1HeapWastePercent指定结束混合回收时空Region的比例

G1在混合回收时,对Region的回收都是基于复制算法进行的。首先会把要回收的Region里的存活对象放入其他Region,然后清理原Region的对象,这样在回收过程中就会不断空出新的Region。一旦空出的Region达到默认堆内存大小的5%,那么此时就会结束本次的混合回收。

由于G1整体(新生代和老年代)是基于复制算法进行Region垃圾回收的,所以不会出现内存碎片的问题。G1不需要像CMS那样,在标记清理后再进行内存碎片的整理。

三.G1MixedGCLiveThresholdPercent指定被回收Region的存活对象占比

默认值是85%,意思是要回收的Region的存活对象大小占比要小于85%。如果一个Region中,其存活对象都占了该Region大小的85%以上。那么再把85%大小的存活对象都拷贝到另一个Region中的成本就会很高,所以就没必要回收这种Region了。

(5)回收失败时的Full GC

在进行Mixed GC回收时,新生代和老年代都是基于复制算法进行回收的。也就是Mixed GC会把要回收Region的存活对象拷贝到其他空闲的Region。如果在拷贝过程中发现没有空闲的Region可存放Mixed GC的存活对象了,那么就会触发一次Mixed GC失败时的Full GC。

一旦触发Mixed GC失败时的Full GC,就会停止系统程序。然后采用单线程进行标记、清理和压缩整理,清空出一批Region,这个过程会非常慢。

(6)问题

结合ParNew + CMS组合的JVM GC优化思路:

一.G1垃圾回收器中值得优化的地方(合理停顿 + 少MGC + 避免FGC )

二.什么情况可能会导致G1频繁触发Mixed GC(老年代占45%触发MGC)

三.如何减少MGC频率(S区足够大 +提高触发占比 + 不过早结束MGC)

4.如何基于G1垃圾回收器优化性能

(1)案例背景

一个百万级注册用户的在线教育平台,主要目标用户群体是中小学生。注册用户大概是几百万,日活用户大概是几十万。

系统的业务流程也不复杂,普通用户浏览课程详情、下单付费、选课排课等低频行为几乎不用考虑。对于这样一个在线教育平台,其高频行为就是上课。

这个平台的使用人群是中小学生,该用户群体周一到周五白天要上学,放学后到八九点才会频繁使用平台,周末也会频繁地使用这个平台。

所以在每天晚上两三小时高峰期会有几十万日活用户来该教育平台上课,甚至可认为白天几乎没什么流量,而99%的流量都集中在晚上两三小时。

(2)系统核心业务流程分析

接着来明确一下,用户在上课时主要高频使用的这个系统的哪些功能。假设用户使用该系统时,核心的业务流程就是游戏互动环节。通过游戏互动让用户感兴趣、愿意学、保持注意力、提升学习效果。

也就是说,这个游戏互动功能,会承载用户高频率、大量的互动点击。比如在完成某任务时要点击很多按钮、频繁的进行互动。然后系统需要接收大量互动请求,并且记录用户的互动过程和互动结果。比如系统需要记录下用户完成了多少任务、做对了几个、做错了几个等。

(3)系统的运行压力

现在开始来分析一下这个系统运行时对内存使用产生的压力。核心就是在晚上两三小时高峰期内,每秒钟会有多少请求,每个请求会产生多少对象、占用多少内存,每个请求要处理多长时间。

一.首先估算晚上高峰期几十万用户使用系统时每秒会产生多少请求

假设晚上3小时高峰期内共有60万活跃用户,平均每个用户使用1小时。那么每小时会有20万活跃用户进行在线学习,这20万用户会进行大量互动操作。

假设一用户每分钟进行1次互动操作,那么一个用户一小时内就会进行60次互动操作,所以20万用户在1小时内会进行1200万次互动操作。平均到每秒大概就是3000次左右的互动操作,也就是系统每秒要处理3000并发请求。根据经验,一般需要部署5台4核8G机器,每台机器每秒处理600请求。这个压力可以接受,一般不会导致宕机的问题。

二.然后估算每个请求会产生多少个对象

一次互动请求不会有太复杂的对象,主要记录用户的一些互动过程。比如用户每完成一个活动,就给用户累加一些"XX币","XX宝石"等。所有一次互动请求大致会创建几个对象,占据几K的内存。一个对象大概几十个字段,每个Long字段8字节,一个对象就几百字节。加上系统其他功能的运行,一次请求假设涉及十几个这样的对象。那么一次请求涉及创建的对象占5K,一秒600请求就会占用3M内存。

(4)在线教育系统背景总结

在介绍百万用户在线教育平台的G1垃圾回收优化案例前,先分析了:系统核心业务、高峰压力、机器部署、每秒请求数、每秒内存压力。

接下来会基于每秒内存使用压力,结合G1的运行原理,进行如下分析:

G1垃圾回收机制会如何运行,在这个运行过程中可能会产生哪些问题;G1垃圾回收器在使用时有哪些地方是值得优化的;如何对G1的一些参数进行优化来调整垃圾回收性能;我们应该要合理分析系统的内存压力,然后合理优化JVM的参数,尽可能降低JVM GC的频率,同时降低JVM GC导致的系统停顿的时间。

(5)G1垃圾回收器的默认内存布局

系统采用了5台4核8G机器来部署,每台机器每秒会有600个请求占用3M的内存。假设给每台机器上的JVM分配了4G的堆内存,并且使用G1垃圾回收器。其中新生代默认初始占比为5%,最大占比为60%。每个Java线程的栈内存为1M,元数据区域(永久代)的内存为256M。此时JVM参数如下:

 -Xms4096M -Xmx4096M  -Xss1M 
 -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseG1GC

-XX:G1NewSizePercent设置新生代初始占比,采用默认值5%。

-XX:G1MaxNewSizePercent设置新生代最大占比,采用默认值60%。

此时堆内存为4G,G1会除以2048,计算出每个Region的大小为2M。刚开始新生代只占5%的Region,即只有100个Region共200M内存空间。如下图示:

(6)GC停顿时间如何设置

在G1垃圾回收器中有一个至关重要的参数会影响到GC的表现,就是-XX:MaxGCPauseMills,默认值是200毫秒。这个参数指定每次触发GC时导致的系统停顿时间期望不超过200毫秒,这个参数可以先保持默认值。

(7)到底多长时间会触发新生代GC

当系统运行起来后,会不停地在新生代的Eden区域内分配对象。按照前面的估算,每秒会分配3M大小的对象。如下图示:

一.一些问题和假设

问题一:

Eden区空间不够,就触发新生代GC,那什么时候Eden区会内存不够?

问题二:

-XX:G1MaxNewSizePercent限定了新生代最多占用堆内存60%的大小。那么难道必须随着系统运行一直给新生代分配更多的Region,直到新生代占据60%的Region后,无法再分配Region才触发新生代GC?G1肯定不是这么做的。

二.G1的运行原理

假设在这个系统里,G1回收300个Region(600M内存)大概需要200ms。那么很有可能系统在运行时呈现出如下的效果:系统运行时每秒创建3M对象,大概1分钟就会塞满100个Region(200M)。如下图示:

此时很可能G1会觉得:要是现在就触发一次新生代GC。那么回收区区200M只需要大概几十ms,最多就让系统停顿几十ms而已。这与启动时-XX:MaxGCPauseMills参数设定的200ms停顿时间相差甚远。所以要是现在就触发新生代GC,那么久可能会导致:回收完成后1分钟,再次占满新生代的这100个Region,又要触发新GC。这样每分钟都要执行一次新生代GC,过于频繁了,没这个必要。

因此G1可能就会觉得:还不如给新生代先增加一些Region。然后让系统继续运行着,在新增加的新生代Region中分配对象好了,这样就不用过于频繁的触发新生代GC。如下图示:

然后系统继续运行,一直到可能300个Region都占满了。此时通过计算发现回收这300个Region大概需要200ms,那么可能这个时候才会触发一次新生代的GC。

由此可见,其实G1是很动态灵活的。它会根据设定的GC停顿时间给新生代不停分配更多Region。然后到一定程度,感觉差不多了,才会触发新生代GC。从而保证新生代GC时导致的系统停顿时间在预设范围内,而且也避免了频繁的新生代GC。

需要注意的是:

分配多少Region给新生代、多久触发一次新生代GC、每次耗费多长时间。G1并不能确定,必须通过工具查看系统实际情况才知道,无法提前预知。

G1的运行原理总结:

G1会根据预设的GC停顿时间,给新生代分配一些Region。然后到一定程度才触发GC,并且把GC停顿时间控制在预设范围内,尽量避免一次性回收过多Region导致GC停顿时间超出预期。

(8)新生代GC如何优化

垃圾回收器是一代比一代先进的,虽然内部实现机制越来越复杂,但是优化却越来越简单。

比如对于G1而言:

一.首先给整个JVM的堆区域足够的内存

比如我们在这里就给了JVM超过5G的内存,其中堆内存有4G的内存。

二.接着合理设置-XX:MaxGCPauseMills参数

如果这个参数设置太小了:

那么说明每次GC停顿时间可能特别短。此时G1可能在发现几十个Region占满时,就要开始触发新生代GC。从而导致新生代GC频率特别频繁。比如如果设置每次停顿30毫秒,那么可能会每30秒触发一次新生代GC。

如果这个参数设置过大了:

那么G1会允许不停地在新生代分配新对象。然后积累很多对象,再一次性回收几百个Region。此时可能一次GC停顿时间就会达到几百毫秒,但GC的频率很低。比如每30分触发一次新生代GC,但每次停顿500毫秒。

所以预期的GC停顿时间到底如何设置,需要结合系统压测工具、GC日志、内存分析工具来考虑,尽量别让系统的GC频率太高,同时每次GC停顿时间也别太长。

(9)Mixed GC如何优化

一.频繁触发Mixed GC的关键

新生代对象进入老年代的几个条件是:YGC后存活对象太多没法放入Survivor区 + 对象年龄太大 + 动态年龄判定规则。

Mixed GC的触发条件是:老年代在堆内存里占比超过45%。

在新生代对象进入老年代的几个条件其中比较关键的就是:新生代GC后存活对象太多无法放入Survivor区和动态年龄判定规则,因为这两个条件可能让很多对象快速进入老年代。一旦老年代达到占用堆内存45%的阈值,那么就会频繁触发Mixed GC。

所以Mixed GC本身很复杂,很多参数可以优化。但是优化Mixed GC的核心不是优化它的参数,而是和前面分析的一样。尽量避免对象过快进入老年代,避免频繁触发Mixed GC,就能实现优化。

二.合理设置-XX:MaxGCPauseMills避免频繁触发Mixed GC

由于G1和ParNew + CMS的组合是不同的,那应该如何来优化参数呢?其实核心的还是-XX:MaxGCPauseMills这个参数。

如果-XX:MaxGCPauseMills参数设置的值很大,导致系统运行很久,新生代都占用堆内存的60%时才触发新生代GC。那么存活下来的对象可能就会很多,导致Survivor区放不下那么多对象。于是这些存活下来的对象就会全部进入老年代,或者存活下来的对象比较多,达到S区的50%,触发动态年龄判定规则,那么也会导致下一次新生代GC的存活对象快速进入老年代。

所以核心还是在于调节-XX:MaxGCPauseMills这个参数的值。在保证新生代GC不太频繁的同时,还得考虑每次GC后有多少存活对象。避免存活对象太多快速进入老年代,频繁触发Mixed GC。

5.问题汇总

问题一:

一个广告系统,使用的就是G1垃圾回收器。因为堆内存有30G,传统回收器可能会造成很大的停顿,所以使用了G1。

答:G1非常适合超大内存的机器。因为内存太大,不用G1会导致新生代每次GC回收垃圾太多,停顿太长。使用G1则可以指定每次GC停顿时间,每次回收一部分Region。

问题二:

从GC效果上看,G1最明显的特点就是可以预测STW的时间。G1为了达到这个效果,抛弃传统分代内存,分成各个小内存块Region。针对这些Region计算垃圾回收价值,然后选某些性价比高的进行GC,以便在预先设定的GC时间内完成GC。所以是不是G1可以用在对STW特别敏感的业务上?比如实时通信等追求低延迟响应的业务。

答:是的。还有就是那种大内存机器,比如16G,32G的机器部署的系统。大内存机器如果不用G1,那么新生代满时对象太多,一次GC时间太长。而用了G1则可以控制停顿时间,每次只回收部分Region即可。

问题三:

G1按Region回收会不会形成新的内存碎片?

答:不会。Region回收时使用的是复制算法,会将存活对象拷贝到其他Region。然后再对原来的Region直接回收掉全部垃圾。

问题四:

G1分那么多Region,有点像HDFS里的小文件,小文件太多会影响性能。但是为什么G1的性能会比之前那些更好?

答:划分为很多的Region,回收时按照设定只能停顿系统20ms。所以就会挑选少量Region来回收,这样可以控制垃圾回收的停顿时间。如果按照ParNew + CMS组合简单分代划分,必须回收整个新生代。这时每次GC回收的内存区域大了,必然要停顿更久时间。

问题五:

一个Spring Boot应用在8G内存开发机上跑,启动需要加载的类特别多。每次JVM一启动,新生代就以每秒10M的速度增长,光启动就要十分钟。因为发现启动期间就进行了两次Full GC,半小时执行了十几次YGC。于是就调整了新老比例为2比1,共分配4G。之后FGC一直为0, YGC半小时只有两次,启动时间也降为1分钟以内。

答:是的,这就是典型的新生代内存不足导致的。系统启动时要创建一堆对象,发现新生代不够。于是频繁YGC,很多对象进入到老年代。然后老年代又不足,又要对老年代Full GC。最后就出现十多次YGC + 几次Full GC。

由于GC太多会导致系统启动速度很慢。优化比例后,新生代内存充足,很多对象直接进入新生代不用进老年代。于是最多就是少数YGC回收一部分对象,也不会有FGC。GC次数减少了,那系统启动速度也就快了。

问题六:

G1垃圾回收器也应该合理分配新生代的占比,保证S区足够大。不让存活对象很快进入老年代,不让老年代很快占到45%。如果老年代不那么快占到45%,自然就可以减少混合回收。

问题七:

一.G1混合回收在第四个阶段会进行多次混合回收,这个多次混合回收的间隔是由G1自己控制的。

二.空闲的Region数量达到堆内存5%就会停止回收,即默认最多进行8次混合回收。但可能到了4次,发现空闲Region达到5%就不进行混合回收了。

三.Mixed GC回收失败时Full GC,应该是采用Serial Old回收器。

文章转载自: ++东阳马生架构++

原文链接: https://www.cnblogs.com/mjunz/p/18642777

体验地址: 引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

相关推荐
东阳马生架构10 小时前
JVM实战—11.OOM的原因和模拟以及案例
jvm
Java 第一深情10 小时前
面试题解,JVM中的“类加载”剖析
java·jvm
Java 第一深情13 小时前
面试题解,Java中的“对象”剖析
java·jvm
大G哥14 小时前
JVM实战—8.如何分析jstat统计来定位GC
jvm
啊烨疯狂学java15 小时前
0101java面经
java·jvm·算法
喵了个咪82718 小时前
JVM调优(内存、GC、JVM参数)
jvm
菜菜小蒙21 小时前
【Linux】多线程
java·开发语言·jvm
东阳马生架构1 天前
JVM实战—10.MAT的使用和JVM优化总结
jvm
李老头探索2 天前
深入解析 JVM vs JDK vs JRE:三者区别与联系详解
java·开发语言·jvm