JVM实战—5.G1垃圾回收器的原理和调优
大纲
1.G1垃圾回收器的工作原理
2.G1分代回收原理—性能为何比传统GC好
3.使用G1垃圾回收器时应如何设置参数
4.如何基于G1垃圾回收器优化性能
5.问题汇总
1.G1垃圾回收器的工作原理
(1)ParNew + CMS的组合有哪些痛点
(2)G1垃圾回收器
(3)G1如何实现垃圾回收的停顿时间是可控的
(4)Region可能属于新生代也可能属于老年代
(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垃圾回收器的设计思想
(2)如何设定G1对应的内存大小
(3)新生代Region还会分Eden区和Survivor区
(4)G1的新生代垃圾回收
(5)对象什么时候进入老年代
(6)大对象Region
(7)总结
(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的动态内存管理策略总结
(2)何时触发新生代 + 老年代的混合垃圾回收
(3)G1混合垃圾回收的过程
(4)G1垃圾回收器的一些参数
(5)回收失败时的Full GC
(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)案例背景
(2)系统核心业务流程分析
(3)系统的运行压力
(4)在线教育系统背景总结
(5)G1垃圾回收器的默认内存布局
(6)GC停顿时间如何设置
(7)到底多长时间会触发新生代GC
(8)新生代GC如何优化
(9)Mixed GC如何优化
(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回收器。
问题八:
使用G1垃圾回收器时,值得优化的地方。
对于新生代:
目标是避免短期存活的对象进入老年代。
一.预估系统每次Young GC后存活对象,确保Survivor区能放得下
二.高峰期避免满足动态年龄判断条件,导致短期存活对象进入老年代
三.大对象有大对象的Region,不占用老年代空间,基本不用考虑
对于老年代:
一.合理设置预测的停顿时间,并非越小越好
如果过小,有可能多次回收效果不大,导致频繁新生代GC,很多短期对象进入老年代。然后老年代很快达到45%触发MGC,可能出现回收失败导致FGC。
二.适当提高-XX:G1HeapWastePercent比例
避免万一真的遇到了高峰期,短期存活对象进入老年代。但是回收时,进行了几次混合回收就刚好达到了5%。但是在老年代Region中可能还存在某些短期存活对象没有被回收,避免过早结束混合回收,导致频繁新生代GC。
三.-XX:G1MixedGCLiveThresholdPercent可忽略
降低这个回收Region的垃圾对象占比可能会让Region的回收效率更高,但也可能导致短期存活对象驻留内存时间过长,进入老年代的风险。
问题九:
(1)使用G1垃圾回收时,值得优化的地方
JVM优化要让短命对象在新生代回收,长期存活对象尽早进入老年代,G1的优化思路亦是如此。
需要根据具体业务系统:首先合理分配老年代和新生代大小、新生代Eden区和Survivor区大小。其次合理设置G1的MaxGCPauseMills大小。MaxGCPauseMills太小容易造成GC频繁,影响系统的吞吐量。MaxGCPauseMills太大会增大系统的停顿时间,影响用户体验。
(2)什么时候会导致G1频繁触发Mixed混合垃圾回收
一.InitiatingHeapOccupancyPercent设置太小
二.新生代和老年代空间设置不合理,导致进入老年代对象太多,从而频繁达到MixedGC的条件
(3)如何尽量减少Mixed GC的频率
触发MixedGC条件:老年代达到InitiatingHeapOccupancyPercent的值。
一.让垃圾对象尽量在新生代就被回收掉,尽量让短命对象不进老年代
这就要根据具体业务系统来合理设置新生代Eden大小和Survivor的大小。
二.将老年代设置较小的值或提高参数InitiatingHeapOccupancyPercent的值
这样就可以使触发MixedGC概率降低,但这样也可能会存在一些问题。
设置老年代内存为较小的值存在的问题:
如果有较多的需要长期存活的对象的情况下,容易FGC或直接OOM了。
提高InitiatingHeapOccupancyPercent的问题:
虽然降低了MixedGC的频率,但导致老年代存在过多的对象。这样每次进行老年代回收时,就会增加:"并发标记阶段"的计算负担和"混合回收阶段"计算和预估的负担,也就是不太适合CPU负载较高的计算型业务的系统。
问题十:
公司业务系统使用G1分析:比如机器堆内存5G,新生代最多占堆内存的60%,即3G。1秒产生3M对象,1分就产生180M对象。
Eden区和Survivor区按默认的8 : 1 : 1比例来计算。Eden区有2400M,占满需要14分,故每14分就会进行一次新生代回收。每个Survivor区有300M内存,每次新生代回收有180M对象会存活下来。此时超过S区50%,所以每次新生代回收存活的180M对象会进入老年代。
也就是每隔14分钟就会有180M的垃圾进入老年代,这样一个小时大概有800M会进入老年代。老年代达到45%时,即2G * 45% = 900M,会进行混合回收。大概1个小时零10分后,会进行一次混合回收。
这个系统其实还可以,系统性能不会太差。
问题十一:
传统的垃圾收集器是通过调整Eden区和Survivor区的大小来控制。而G1则比较先进,直接指定一个期望的停顿时间。选择停顿时间的标准是:既不能导致频繁触发YGC,也不能导致一次回收过多对象停顿时间过长。
所以还得通过工具来调试出一个最适合自己系统的停顿时间,通过工具检测,要得出一个停顿多久可以回收多少的内存大小的指标。再根据这个指标和业务系统生成垃圾的速率设置合理的停顿时间。
问题十二:
G1这种垃圾回收器到底在什么场景下适用?哪些场景适合采用ParNew + CMS垃圾回收器?
答:一.G1压缩内存空间会比较有优势,适合产生大量碎片的应用。
二.G1能够设置可以预期的GC停顿时间,对低延时应用更有优势。
三.其他垃圾收集器对大内存回收耗时较长;G1对内存分成多块区域,能根据预期停顿时间选性价比高的区域来回收,G1适用JVM内存占用大的应用。
四.ParNew + CMS回收器适用内存小、对象在新生代中存活短的应用。比如贷款业务,申请额度后就在后台处理了,有额度以后再通知用户。
问题十三:
使用系统的用户其实并不关心什么GC频率,但关心的是使用的系统卡不卡,处理速度快不快。系统卡不卡会受-XX:MaxGCPauseMills影响,系统处理速度快不快会受GC的频率影响。
GC的频率高,对应的应该是系统的吞吐量低。GC频率高说明用来处理垃圾回收的时间变多,处理业务的时间变少。比如原先每秒可以取出1万条数据,现在只能取出2千条。