现象
服务刚刚启动时 GC 次数较多,最大空间剩余很多但是依然发生 GC,这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,如下图所示:
原因
在 JVM 的参数中 -Xms 和 -Xmx 设置的不一致,在初始化时只会初始 -Xms 大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次GC。具体是通过 ConcurrentMarkSweepGeneration::compute_new_size()方法计算新的空间大小:
void ConcurrentMarkSweepGeneration::compute_new_size() {
assert_locked_or_safepoint(Heap_lock);
// If incremental collection failed, we just want to expand
// to the limit.
if (incremental_collection_failed()) {
clear_incremental_collection_failed();
grow_to_reserved();
return;
}
// The heap has been compacted but not reset yet.
// Any metric such as free() or used() will be incorrect.
CardGeneration::compute_new_size();
// Reset again after a possible resizing
if (did_compact()) {
cmsSpace()->reset_after_compaction();
}
}
另外,如果空间剩余很多时也会进行缩容操作,JVM 通过 -XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机,例如扩容便是使用 GenCollectedHeap::expand_heap_and_allocate() 来完成的,代码如下:
HeapWord* GenCollectedHeap::expand_heap_and_allocate(size_t size, bool is_tlab) {
HeapWord* result = NULL;
if (_old_gen->should_allocate(size, is_tlab)) {
result = _old_gen->expand_and_allocate(size, is_tlab);
}
if (result == NULL) {
if (_young_gen->should_allocate(size, is_tlab)) {
result = _young_gen->expand_and_allocate(size, is_tlab);
}
}
assert(result == NULL || is_in_reserved(result), "result not in heap");
return result;
}
整个伸缩的模型理解可以看这个图,当 committed 的空间大小超过了低水位 / 高水位的大小,capacity 也会随之调整:
策略
定位
观察 CMS GC 触发时间点 Old/MetaSpace 区的 committed 占比是不是一个固定的值,或者像上文提到的观察总的内存使用率也可以。
解决
尽量将成对出现的空间大小配置参数设置成固定的,如 -Xms 和 -Xmx,-XX:-MaxNewSize 和 -XX:NewSize,-XX:MetaSpaceSize 和 -XX:MaxMetaSpaceSize 等。
小结
一般来说,我们需要保证 Java 虚拟机的堆是稳定的,确保 -Xms 和 -Xmx 设置的是一个值(即初始值和最大值一致),获得一个稳定的堆,同理在 MetaSpace 区也有类似的问题。不过在不追求停顿时间的情况下震荡的空间也是有利的,可以动态地伸缩以节省空间,例如作为富客户端的 Java 应用。