part 5
本comment希望能系统的探索一下GC发生的时机,以及各个GC的具体工作内容(流程),GC包括Minor GC和Major GC,下面将分别看看Minor GC和Major GC会在什么时候执行、怎么执行的,也就是希望能了解触发GC的条件和GC原理。
已经在前面的内容中说过JVM是如何支持GC工作的,简单来说就是在create_vm的时候创建一个VMThread,VMThread有一个任务队列,VMThread会等待任务队列里面存储任务然后拿出来执行,当任务队列中已经没有可以执行的任务的时候就wait,直到被其他的线程notify,然后接着处理任务;任务队列里面存储着VMOperationQueue类型的任务,有很多类型的VMOperationQueue,VM_GC_Operation代表的就是GC操作,所以主要来关注VM_GC_Operation,VM_GC_Operation也有很多子类,下面的图片展示了VM_GC_Operation的子类情况:
<img width="665" alt="2018-11-12 10 30 50" src="https://user-images.githubusercontent.com/16225796/48353562-b63c7d80-e6ca-11e8-895d-56951e6eab18.png">
其中VM_CollectForAllocation表示内存申请失败,它有三个子类,分别是VM_GenCollectForAllocation、VM_ParallelGCFailedAllocation、VM_G1OperationWithAllocRequest;带Full字符的Operation代表是一次FullGC,有VM_GenCollectFull、VM_G1CollectFull;VM_ParallelGCSystemGC虽然不带Full,但是也是FullGC操作;下面来看看触发这些VM_GC_Operation的时机到底是什么时候。
VM_GenCollectForAllocation
可以在collectorPolicy.cpp的mem_allocate_work函数里面发现除了了一个VM_GenCollectForAllocation;mem_allocate_work函数用于申请内存空间,前面的文章也分析过这个函数,简单来说,这个函数将首先在YoungGen里面申请内存,如果无法得到满足,那么就去OldGen试试,如果OldGen也不可以满足话,那么就去尝试扩展堆之后再试试,如果还是不行,那就只能触发一个VM_GenCollectForAllocation了;
<img width="889" alt="2018-11-12 10 52 13" src="https://user-images.githubusercontent.com/16225796/48354879-bee28300-e6cd-11e8-8c4c-8947845e7a25.png">
VMThread::execute函数会将这个VM_GenCollectForAllocation放到VMThread的任务队列里面去,VMThread就会执行这个VM_GenCollectForAllocation的doit函数,下面来看看VM_GenCollectForAllocation的doit函数的具体实现:
void VM_GenCollectForAllocation::doit() {
SvcGCMarker sgcm(SvcGCMarker::MINOR);
GenCollectedHeap* gch = GenCollectedHeap::heap();
GCCauseSetter gccs(gch, _gc_cause);
_result = gch->satisfy_failed_allocation(_word_size, _tlab);
assert(gch->is_in_reserved_or_null(_result), "result not in heap");
if (_result == NULL && GCLocker::is_active_and_needs_gc()) {
set_gc_locked();
}
}
satisfy_failed_allocation函数前面的文章也已经说过了,再总结一下这个函数的具体工作;
- (1)、首先判断是有其他的线程触发了GC,如果是的话,那么本次GC就不能继续了,但是退出前试试能不能扩展堆,如果可以的话说不定就可以在扩展堆之后成功申请到需要的空间了,如果这个时候不能扩展堆的话,那么就只能退出等其他的线程GC完成了;
- (2)、判断是否可以增量进行GC,如果可以的话,那么就执行一次Minor GC,否则执行一次不回收soft reference的Full GC;如果这次GC之后可以成功申请到内存了
- (3)、如果(2)结束之后还是无法申请到足够的内存,那么就要进行一次彻底的FullGC,这次GC将要把soft reference都清理掉;
总结一下,VM_GenCollectForAllocation会在内存申请失败的时候进行工作,它可能触发Minor GC和FullGC,首先是Minor GC,如果Minor GC并不奏效,那么就要进行FullGC;
VM_ParallelGCFailedAllocation
VM_GenCollectForAllocation工作在DefNew,是SerialGC的年轻代;VM_ParallelGCFailedAllocation工作在ParallelScavengeHeap,ParallelScavengeHeap是UseParallelGC和UseParallelOldGC的年轻代,属于"吞吐量"GC,该类型的GC注重的是系统的吞吐量,和CMS注重"响应时间"不同,"吞吐量"类型GC可以设定用于GC的时间,JVM会自动调整堆来满足要求;
可以在parallelScavengeHeap的mem_allocate函数里面看到触发了VM_ParallelGCFailedAllocation操作;
<img width="722" alt="2018-11-12 11 22 23" src="https://user-images.githubusercontent.com/16225796/48356738-ea676c80-e6d1-11e8-8c1e-12c85888c3f2.png">
mem_allocate函数先从YounYoungGen申请内存,如果无法得到满足,那么就去OldGen去申请内存;如果还是无法满足要求,那么就触发一个
VM_GenCollectForAllocation操作,来看看VM_GenCollectForAllocation的doit函数的实现;
void VM_ParallelGCFailedAllocation::doit() {
SvcGCMarker sgcm(SvcGCMarker::MINOR);
ParallelScavengeHeap* heap = ParallelScavengeHeap::heap();
GCCauseSetter gccs(heap, _gc_cause);
_result = heap->failed_mem_allocate(_word_size);
if (_result == NULL && GCLocker::is_active_and_needs_gc()) {
set_gc_locked();
}
}
ParallelScavengeHeap::failed_mem_allocate函数将会处理接下来的工作,下面来分析一下ParallelScavengeHeap::failed_mem_allocate这个函数的具体实现细节;
// Failed allocation policy. Must be called from the VM thread, and
// only at a safepoint! Note that this method has policy for allocation
// flow, and NOT collection policy. So we do not check for gc collection
// time over limit here, that is the responsibility of the heap specific
// collection methods. This method decides where to attempt allocations,
// and when to attempt collections, but no collection specific policy.
HeapWord* ParallelScavengeHeap::failed_mem_allocate(size_t size) {
assert(SafepointSynchronize::is_at_safepoint(), "should be at safepoint");
assert(Thread::current() == (Thread*)VMThread::vm_thread(), "should be in vm thread");
assert(!is_gc_active(), "not reentrant");
assert(!Heap_lock->owned_by_self(), "this thread should not own the Heap_lock");
// We assume that allocation in eden will fail unless we collect.
// First level allocation failure, scavenge and allocate in young gen.
GCCauseSetter gccs(this, GCCause::_allocation_failure);
const bool invoked_full_gc = PSScavenge::invoke();
HeapWord* result = young_gen()->allocate(size);
// Second level allocation failure.
// Mark sweep and allocate in young generation.
if (result == NULL && !invoked_full_gc) {
do_full_collection(false);
result = young_gen()->allocate(size);
}
death_march_check(result, size);
// Third level allocation failure.
// After mark sweep and young generation allocation failure,
// allocate in old generation.
if (result == NULL) {
result = old_gen()->allocate(size);
}
// Fourth level allocation failure. We're running out of memory.
// More complete mark sweep and allocate in young generation.
if (result == NULL) {
do_full_collection(true);
result = young_gen()->allocate(size);
}
// Fifth level allocation failure.
// After more complete mark sweep, allocate in old generation.
if (result == NULL) {
result = old_gen()->allocate(size);
}
return result;
}
这个函数分下面几个步骤来处理Allocation Fail;
- (1)、用PSScavenge::invoke()去做"scavenge"的工作,可能是一次Minor GC,也可能是FullGC,如果触发了一次FullGC,那么该函数就会返回true,否则返回false;完成之后,尝试从新生代从新申请空间,如果不能成功,则进行第(2)步;
- (2)、如果PSScavenge::invoke()做的是一次Minor GC,那么此时就要做一次FullGC,但是先不回收soft reference;结束之后重新尝试去新生代申请空间,如果不能满足,那么继续第(3)步;
- (3)、既然新生代无法申请到空间,那去老年代试试吧,如果在老年代成功申请到了空间,那么就结束,否则继续第(4)步;
- (4)、在一次FullGC之后,从新生代和老年代均无法获取到空间,那么就只能把soft reference清理一下了,也就是做一次清理soft reference的FullGC;之后再尝试从新生代申请空间,如果还是无法满足,那么执行第(5)步;
- (5)、第(4)步还是无法申请成功的话,那么就尝试去老年代试试,如果还不行,那就交给上层处理吧(oom)
来看看PSScavenge::invoke()的具体实现细节;
<img width="862" alt="2018-11-13 4 11 37" src="https://user-images.githubusercontent.com/16225796/48399751-78d5ff80-e75f-11e8-900f-00de9be35be6.png">
PSScavenge::invoke_no_policy()首先将被调用进行一次MinorGC,在MinorGC的过程中可能有一些对象达到了晋升阈值,但是可能老年代因为空间不够的问题无法将所有晋升的对象都放到老年代,这个时候就发生了Promotion Fail;因为Scavenge GC的一个特点是可以自动调整各个分代的大小以满足设定的参数,这个过程较为复杂,可以在PSScavenge::invoke_no_policy()里面找到这些代码;Minor GC的过程大概和DefNew是一样的,但是和DefNew不一样的地方就是ParallelScavengeHeap使用了多线程来做GC,所以代码要复杂很多,但是流程还是那样,首先标记GCRoot,然后根据GCRoot去遍历存活对象,之后标记-清除;
接着回到PSScavenge::invoke(),need_full_gc用于判断是否需要进行FullGC,刚才已经试图去做一些MinorGC了,但是MinorGC可能根本没有执行,如果当前线程发现已经有其他线程在做GC了,那么就会直接退出;need_full_gc的判断由两部分组成,首先是PSScavenge::invoke_no_policy()的结果,也就是PSScavenge::invoke_no_policy()是否真的执行了MinorGC,如果没有执行,那么就有必要执行一次FullGC;如果PSScavenge::invoke_no_policy()成功执行了,那么就看policy->should_full_GC,这个policy是PSAdaptiveSizePolicy;下面来看看这个函数的判断:
// If the remaining free space in the old generation is less that
// that expected to be needed by the next collection, do a full
// collection now.
bool PSAdaptiveSizePolicy::should_full_GC(size_t old_free_in_bytes) {
// A similar test is done in the scavenge's should_attempt_scavenge(). If
// this is changed, decide if that test should also be changed.
bool result = padded_average_promoted_in_bytes() > (float) old_free_in_bytes;
log_trace(gc, ergo)("%s after scavenge average_promoted " SIZE_FORMAT " padded_average_promoted " SIZE_FORMAT " free in old gen " SIZE_FORMAT,
result ? "Full" : "No full",
(size_t) average_promoted_in_bytes(),
(size_t) padded_average_promoted_in_bytes(),
old_free_in_bytes);
return result;
}
判断条件很简单,如果发现YoungGen里面等待晋升到OldGen的对象大小大于oldGen的空闲空间,那么就有必要执行FullGC了;接着看进行FullGC的代码,UseParallelOldGC用于判断老年代使用的堆类型,如果我们在JVM启动的时候使用了-XX:+UseParallelOldGC,那么新生代和老年代的组合就是(Parallel Scavenge + Parallel Old),如果使用的是-XX:+UseParallelGC,那么新生代和老年代的组合就是(Parallel Scavenge + Serial Old);这里假设使用了-XX:+UseParallelGC,那么就看PSMarkSweep::invoke_no_policy(clear_all_softrefs);而Serial Old的GC过程前面的文章已经分析过就不继续了。
现在回到ParallelScavengeHeap::failed_mem_allocate函数,看看剩下的部分;PSScavenge::invoke()执行过后,可能进行了一次MinorGC,或者是FullGC,可能将soft reference清理掉了,但是总得来说执行了PSScavenge::invoke()之后已经清理了一波垃圾了,young_gen()->allocate(size)试图从新生代申请空间;如果申请失败,那么就看刚才是否做了FullGC,如果做了,那么就只能oom了,否则通过do_full_collection(false)做一次FullGC,但是soft reference依然还在;接着分别从young 和 old去申请空间,如果还是无法满足要求,那么就通过do_full_collection(true)来做一次清理FullGC,并且将soft reference清理掉,然后再从young 和 old中去试图申请内存,如果还是无法申请成功,那么就交给上层处理吧。(OOM)
VM_G1OperationWithAllocRequest
VM_G1OperationWithAllocRequest有两个子类:VM_G1CollectForAllocation和VM_G1IncCollectionPause,属于G1的内容,暂时不做分析,后续专门分析G1的相关实现细节;
VM_GenCollectFull
VM_GenCollectFull用于支持一些外部的GC命令,比如System.gc(),可以在GenCollectedHeap::collect_locked函数里面发现VM_GenCollectFull操作:
void GenCollectedHeap::collect_locked(GCCause::Cause cause, GenerationType max_generation) {
// Read the GC count while holding the Heap_lock
unsigned int gc_count_before = total_collections();
unsigned int full_gc_count_before = total_full_collections();
{
MutexUnlocker mu(Heap_lock); // give up heap lock, execute gets it back
VM_GenCollectFull op(gc_count_before, full_gc_count_before,
cause, max_generation);
VMThread::execute(&op);
}
}
genCollectedHeap::collect函数是该操作发生的一个起点,而genCollectedHeap::collect是为了响应类似于System.gc()调用,比如:
JVM_ENTRY_NO_ENV(void, JVM_GC(void))
JVMWrapper("JVM_GC");
if (!DisableExplicitGC) {
Universe::heap()->collect(GCCause::_java_lang_system_gc);
}
JVM_END
这就是一个System.gc()的请求,而调用的就是Universe::heap()->collect函数,Universe::heap()返回的是JVM的一个高层堆管理器,目前JVM里面有三个这样的堆管理器,分别是GenCollectedHeap、ParallelScavengeHeap和G1CollectedHeap,分别对应不同种类型的GC;GenCollectedHeap对应-XX:+UseSerialGC和-XX:+UseConcMarkSweepGC;ParallelScavengeHeap对应-XX:+UseParallelGC和-XX:+UseParallelOldGC以及-XX:+UseParNewGC;G1CollectedHeap对应-XX:+UseG1GC;这些对应关系是在create_vm的时候创建的,关于堆初始化这部分内容将在后续的文章中分析。
和VM_GenCollectFull类似的还有VM_G1IncCollectionPause(G1)、VM_ParallelGCSystemGC(ParallelGC);这里就不分析这两个Operation了。
结论
Minor GC发生的原因较为简单,就是"Allocation Fail";发生"Allocation Fail"的原因就是没有足够的内存了,这个时候就要去做Minor GC,但是,内存不足之后不一定进行Minor GC,可能因为某些原因直接进行了FullGC,在JVM里面有大量的用于判断是否应该在某个分代进行垃圾收集的函数,这些函数将根据一些统计数据来判断是否应该在该区域进行垃圾收集;比如在某次Eden区域分配失败的时候,Old区域就需要判断是否允许Young区进行一次Minor GC,因为进行MinorGC的时候一些符合晋升年龄的对象将会晋升到老年代中来,还有一部分对象因为无法移动到To区域(To区满了或者连续空间小于存活对象大小)也需要提前拷贝到老年代,这些对象转移到老年代对老年代来说是一种负担,并且也是有风险的,比如可能老年代根本没有足够的内存容纳这次Minor GC之后晋升的对象,这个时候MinorGC就要报"Promotion Fail",这就需要开启一次FullGC来回收掉一些不再使用的对象,也可能包括正在使用的soft reference;还有一些发生FullGC的条件(或者说是触发FullGC)本文没有分析到,主要原因是关于G1和CMS还不太了解,CMS和G1是相对复杂的GC,需要花费大量的时间去研究分析以及描述出来。
发生GC有两种原因,主动进行GC和被动进行GC,被动GC就是类似于System.gc(),主动GC发生在allocate的时候,如果可以,应该尽量避免让GC被动GC,因为这会打乱JVM的GC计划,应该相信JVM可以做得足够好,让我们不需要担心GC的问题,这也是Java相比于类似于C/C++的主要优势之一。