背景
最近服务器出现了请求其他接口超时的情况,排查后发现是cms gc过长导致的,最长的CMS gc长达3s,需要定位是什么导致了CMS gc这么长,以及如何解决。
问题定位
根据cms的日志发现remark阶段耗时很长,因为remark阶段是 stop world的所以导致了其他接口超时。问题基本上定位了,下面需要看一下怎么优化了。根据日志可以看到在remark的时候young区基本上已经满了,而remark需要 根据young区作为gc root扫描老年代,所以当young区很大的时候会导致remark阶段超长。
CMSScavengeBeforeRemark参数
是什么
在网上搜了下这种情况发现基本上都是推荐设置CMSScavengeBeforeRemark 参数,那么这个参数是干什么的呢?我们看一下官方的文档是怎么说的:
-XX:+CMSScavengeBeforeRemark
Enables scavenging attempts before the CMS remark step. By default, this option is disabled.
可以看到如果配置了这个参数,jvm会在remark之前尝试执行一次young gc,并且这个参数默认是关闭的。如果执行了young gc,那么对应的新生代的存活数量就会大大减少,remark需要的扫描时间也会减少。
验证
在线上配置了这个参数后,线上的CMS gc的remark阶段的max从3s降到了1s左右。
副作用
根据文档我们可以看到这个参数默认是关闭的,这是为什么呢?因为大部分应用young gc的增长频率并没有那么快,默认情况下开启这个参数反到会增加remark阶段的耗时。
原理
下面我们看一下jvm是如何处理这个参数的。首先找到remark阶段的函数,
void CMSCollector::checkpointRootsFinal(bool asynch,
bool clear_all_soft_refs, bool init_mark_was_synchronous) {
assert(_collectorState == FinalMarking, "incorrect state transition?");
check_correct_thread_executing();
// world is stopped at this checkpoint
assert(SafepointSynchronize::is_at_safepoint(),
"world should be stopped");
TraceCMSMemoryManagerStats tms(_collectorState,GenCollectedHeap::heap()->gc_cause());
verify_work_stacks_empty();
verify_overflow_empty();
SpecializationStats::clear();
if (PrintGCDetails) {
gclog_or_tty->print("[YG occupancy: "SIZE_FORMAT" K ("SIZE_FORMAT" K)]",
_young_gen->used() / K,
_young_gen->capacity() / K);
}
if (asynch) {
if (CMSScavengeBeforeRemark) {
GenCollectedHeap* gch = GenCollectedHeap::heap();
// Temporarily set flag to false, GCH->do_collection will
// expect it to be false and set to true
FlagSetting fl(gch->_is_gc_active, false);
NOT_PRODUCT(GCTraceTime t("Scavenge-Before-Remark",
PrintGCDetails && Verbose, true, _gc_timer_cm, _gc_tracer_cm->gc_id());)
int level = _cmsGen->level() - 1;
if (level >= 0) {
gch->do_collection(true, // full (i.e. force, see below)
false, // !clear_all_soft_refs
0, // size
false, // is_tlab
level // max_level
);
}
}
FreelistLocker x(this);
MutexLockerEx y(bitMapLock(),
Mutex::_no_safepoint_check_flag);
assert(!init_mark_was_synchronous, "but that's impossible!");
checkpointRootsFinalWork(asynch, clear_all_soft_refs, false);
} else {
// already have all the locks
checkpointRootsFinalWork(asynch, clear_all_soft_refs,
init_mark_was_synchronous);
}
verify_work_stacks_empty();
verify_overflow_empty();
SpecializationStats::print();
}
如果满足asynch,并CMSScavengeBeforeRemark开启的时候会进入到gch->do_collection方法。gch是分代回收的堆的抽象,其内部包含了young generation和old generation。同时由于_cmsGen->level()的返回值是1,所以level是0。 下面再来看一下do_collection的实现,这里只截取关键部分代码
int starting_level = 0;
if (full) {
// Search for the oldest generation which will collect all younger
// generations, and start collection loop there.
for (int i = max_level; i >= 0; i--) { // 代码1
if (_gens[i]->full_collects_younger_generations()) {
starting_level = i;
break;
}
}
}
bool must_restore_marks_for_biased_locking = false;
int max_level_collected = starting_level;
for (int i = starting_level; i <= max_level; i++) { // 代码2
if (_gens[i]->should_collect(full, size, is_tlab)) {
if (i == n_gens() - 1) { // a major collection is to happen
if (!complete) {
// The full_collections increment was missed above.
increment_total_full_collections();
}
pre_full_gc_dump(NULL); // do any pre full gc dumps
}
......
}
......
}
由于max_level传入的是0,所以代码1的循环只会执行一次,对应的_gens[0]为ParNewGeneration,full_collects_younger_generations的返回值是false,所以最终starting_level是0。也就是说代码2处的循环也是只会遍历一次,如果_gens[0]的should_collect方法返回的是true,则会往下走进行新生代的回收,因为full=true,所以should_collect返回的一定是true,也就是说一定会进入到ParNewGeneration的collect中。
virtual bool should_collect(bool full,
size_t word_size,
bool is_tlab) {
return (full || should_allocate(word_size, is_tlab));
}
需要注意的是虽然设置了这个参数,JVM一定会进入到新生代的collect方法中,但是这不代表这一定会进行一次回收,这是因为collect方法内部还做了一次特殊的判断,
// If the next generation is too full to accommodate worst-case promotion
// from this generation, pass on collection; let the next generation
// do it.
if (!collection_attempt_is_safe()) {
gch->set_incremental_collection_failed(); // slight lie, in that we did not even attempt one
return;
}
当老年代无法接收新生代晋升上去的对象的时候,这次young gc就会被放弃了。这也就是官方文档上强调attempts