简书 占小狼 转载请注明原创出处,谢谢!
大家新年好,愿你们在新的一年顺利晋升、工资涨涨涨...
之前无意间碰到一个有趣的CMS GC问题,问题很简单,现象很粗暴。
代码
/**
* -Xmx20m -Xms20m -Xmn10m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
* -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75
* -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
*/
public class JVM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
byte[] b1 = new byte[2 * _1MB];
byte[] b2 = new byte[2 * _1MB];
byte[] b3 = new byte[2 * _1MB];
byte[] b4 = new byte[4 * _1MB];
System.in.read();
}
}
现象
程序运行之后,执行jstat -gcutils pid 1000
命令,结果如下:
在JVM参数中已经设置了-XX:+UseCMSInitiatingOccupancyOnly
和 -XX:CMSInitiatingOccupancyFraction=75
只有在老年代的使用率达到75%时才会触发CMS回收,可目前的现象是老年代使用率才60%,就开始不停的GC、不停的GC、不停的GC,GC日志如下:
看这架势,应该是在不停的发生CMS GC了。
原因查找
既然一直在触发CMS,那问题根本因为在触发CMS的条件中,之前以为只要设置了-XX:+UseCMSInitiatingOccupancyOnly
参数,只有在老年代的使用率达到阈值时才会触发。
翻了代码之后才发现,问题没这么简单,触发CMS的判断逻辑位于CMSCollector::shouldConcurrentCollect()
方法中,实现如下:
在设置了-XX:+UseCMSInitiatingOccupancyOnly
参数的前提下,有三种情况会触发:
1、老年代当前使用率是否达到阈值CMSInitiatingOccupancyFraction
;
2、判断当前新生代的对象是否能够全部顺利的晋升到老年代,如果不能,就提早触发一次老年代的收集,这是本案例中不停CMS的根本原因,incremental_collection_will_fail(true)
实现如下:
其中get_gen(0)
指向当前年轻代的堆,因为设置了-XX:+UseParNewGC
,则年轻代的堆实现是ParNewGeneration
,该类继承了DefNewGeneration
,方法collection_attempt_is_safe()
位于DefNewGeneration
类中,实现如下:
前面2个条件先忽略,看最后一个条件,_next_gen
指向老年代的堆,其中promotion_attempt_is_safe()
实现如下:
传入的参数max_promotion_in_bytes
,由年轻代的used
方法计算得到,eden区的使用量 + from区的使用量
size_t DefNewGeneration::used() const {
return eden()->used()
+ from()->used(); // to() is only used during scavenge
}
其中promotion_attempt_is_safe()
方法中的变量
1、available
是老年代的可用内存大小
2、av_promo
每次YGC时晋升到老年代对象大小的平均值
当老年代的可用内存大于av_promo
,或者大于max_promotion_in_bytes
时,说明下次的YGC是安全的,否则返回fasle,提早进行一次CMS操作,释放老年代的空间,以容纳下次YGC晋升上来的对象。
到这里,本文中的例子不断的进行CMS GC的疑惑应该可以解释清楚了。
别忘了,还有第三种情况:
if (CMSClassUnloadingEnabled && _permGen->should_concurrent_collect()) {
bool res = update_should_unload_classes();
if (res) {
if (Verbose && PrintGCDetails) {
gclog_or_tty->print_cr("CMS perm gen initiated");
}
return true;
}
}
前提是设置了-XX:+CMSClassUnloadingEnabled
,而且_permGen永久带的内存使用率达到了阈值CMSInitiatingPermOccupancyFraction
,默认值是92。
即使满足上面2个条件,还需要一层判断update_should_unload_classes()
如果一开始永久代大小没有设置、或者设置的很小,很有可能一开始就执行CMS,这让很多同学表示怀疑,什么都没做,就给我来一次CMS的日志。
网友评论
还有一点我一直不太明白,主动old GC和周期性old GC是否是以同一个函数作为切入点(即调用同一个函数),然后在同一个函数里通过逻辑判断进入不同的分支从而执行不同的GC策略(压缩与否);还是完全是不同的GC程序入口?谢谢狼哥
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
Server compiler detected.
JVM version is 25.111-b14
Non-default VM flags: -XX:CICompilerCount=4 -XX:CMSInitiatingOccupancyFraction=75 -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=6 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:OldSize=10485760 -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseFastUnorderedTimeStamps -XX:+UseParNewGC
Command line: -Xmx20m -Xms20m -Xmn10m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Didea.launcher.port=7534 -Didea.launcher.bin.path=/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
根据文中资料,对比数据,b4 进入新生代时 b1 b2 b3 已经在老年代 。b4 在新生代占4M ,40%,其余三个在 老年代共占 60%。此时CMS GC不是内存分配引起,而是根据“计算结果”判断是否进行“提早CMS收集”....终于懂了,感谢大佬