目录
1、GenCollectedHeap::do_collection
2、CollectedHeap::collect_as_vm_thread
3、VM_CollectForMetadataAllocation
上一篇《Hotspot CMS算法实现总结 (二)》中总结了年轻代和老年代的GC主流程,本篇博客从年轻代和老年代的collect方法的调用方出发,来总结GC是如何被触发的。
1、GenCollectedHeap::do_collection
do_collection方法是调用各Genaration的collect方法的直接上层方法,其主要流程如下:
其中计算start_level的逻辑如下:
DefNewGeneration采用父类Generation的默认实现,返回false,ConcurrentMarkSweepGeneration的实现如下,在默认配置下该方法返回true,即full为true,max_level为1的时候,start_level也为1,max_level是方法入参,表示执行GC的最大level。
//UseCMSCompactAtFullCollection默认为true,表示FullGC时所使用Mark-Sweep-Compact算法
//CollectGen0First默认为false,表示是否在FullGC前收集年轻代
virtual bool full_collects_younger_generations() const {
return UseCMSCompactAtFullCollection && !CollectGen0First;
}
其中是否complete的判断逻辑如下:
bool complete = full && (max_level == (n_gens()-1));
int max_level_collected = starting_level;
complete = complete || (max_level_collected == n_gens() - 1);
即只要max_level等于1的时候complete就为true,如果为1则为false。
补充说明如下:
- 将Reference实例链表中剩余的Reference实例加入pending列表只适用于堆内存压缩式GC,正常的前台GC和后台GC都是在FinalMarking环节完成了该操作。
- 在各Genaration执行gc_epilogue时都会清理ChunkPool,在do_collection中还执行了一遍,主要是释放do_collection方法本身占用的ChunkPool内存。
- 通过老年代collect方法触发的堆内存压缩和前台GC都不会执行compute_new_size方法,该方法就是后台GC的Resizing步骤的核心,会根据当前占用的内存和配置的内存最大空闲率,内存最小空闲率来做必要的扩容和缩容。
- 如果start_level为0时,年轻代GC完成后会将size置为0,当进入老年代的should_collect方法时,因为size为0,该方法返回false,就不会触发老年代的GC。什么情况下start_level为0了?full为false时或者full为true,max_level为0时。
- 结合上面start_level的分析,可知max_level为1的时候,full为false时do_collection只调用年轻代GC,full为true时实际只调用老年代GC,当max_level为0的时候只调用年轻代的collect方法,虽然代码中是循环调用start_level到max_level之间的Genaration的collect方法,但是实际只调用一次。
2、CollectedHeap::collect_as_vm_thread
collect_as_vm_thread方法是给VMThread使用的执行GC的内部方法,该方法假定已经获取了Heap_lock锁,其实现如下:
void CollectedHeap::collect_as_vm_thread(GCCause::Cause cause) {
//校验是VMThread
assert(Thread::current()->is_VM_thread(), "Precondition#1");
//校验获取了Heap_lock
assert(Heap_lock->is_locked(), "Precondition#2");
//临时设置GCCause
GCCauseSetter gcs(this, cause);
switch (cause) {
case GCCause::_heap_inspection: //表示打印堆内存中包含的类的直方图
case GCCause::_heap_dump: //堆内存dump
case GCCause::_metadata_GC_threshold : { //表示元空间内存分配失败
HandleMark hm;
do_full_collection(false); //false表示不需要清除软引用
break;
}
case GCCause::_last_ditch_collection: { //执行过do_full_collection(false)后再次尝试从元空间分配内存,分配失败后就尝试扩容再分配,依然失败就会使用这个GCCause
HandleMark hm;
do_full_collection(true); //true表示清除软引用
break;
}
default:
ShouldNotReachHere(); // Unexpected use of this function
}
}
void GenCollectedHeap::do_full_collection(bool clear_all_soft_refs,
int max_level) {
int local_max_level;
if (!incremental_collection_will_fail(false /* don't consult_young */) &&
gc_cause() == GCCause::_gc_locker) {
local_max_level = 0;
} else {
local_max_level = max_level;
}
do_collection(true /* full */,
clear_all_soft_refs /* clear_all_soft_refs */,
0 /* size */,
false /* is_tlab */,
local_max_level /* max_level */);
if (local_max_level == 0 && gc_cause() == GCCause::_gc_locker &&
incremental_collection_will_fail(false /* don't consult_young */)) {
if (PrintGCDetails) {
gclog_or_tty->print_cr("GC locker: Trying a full collection "
"because scavenge failed");
}
//如果老年代空间不足,则回收老年代
do_collection(true /* full */,
clear_all_soft_refs /* clear_all_soft_refs */,
0 /* size */,
false /* is_tlab */,
n_gens() - 1 /* max_level */);
}
}
在collect_as_vm_thread场景下do_full_collection方法实际就只执行do_collection,其中full为true,max_level就是1,即只回收老年代。collect_as_vm_thread的调用链如下:
3、VM_CollectForMetadataAllocation
VM_CollectForMetadataAllocation是元空间内存分配失败后触发GC的实现,参考其构造方法的调用链,如下:
该类的核心doit方法的主流程如下:
补充说明如下:
- 上述步骤中每次尝试分配成功后就会立即返回,分配失败则继续走下一步。第一步尝试直接分配而不是触发GC是因为一旦元空间不足了,可能会触发多个线程执行VM_CollectForMetadataAllocation,但是VMThread一次只处理一个,即存在其他线程已经触发GC释放了足够的内存,所以这里再做一次尝试分配。
- 第二步判断时,只有CMS算法和G1算法在GC时会卸载Klass,这两种算法在默认配置下返回true,其他的返回false。以CMS的实现为例,在调用该方法判断时,会将MetaspaceGC的_sho