part 1
首选想探索一下GC是怎么开始工作的,或者说,GC到底是以什么样的方式在工作的;java应用在启动的时候会创建一个jvm进程,JVM内部通过调用create_vm来实现,该方法做了大量的工作来创建一个jvm进程,并且将java应用的main方法启动起来,运行在main线程中(主线程);在create_vm中,有一个地方值得关注,下面是thread.cpp中create_vm方法的代码片段:
// Create the VMThread
{ TraceTime timer("Start VMThread", TRACETIME_LOG(Info, startuptime));
VMThread::create();
Thread* vmthread = VMThread::vm_thread();
if (!os::create_thread(vmthread, os::vm_thread)) {
vm_exit_during_initialization("Cannot create VM thread. "
"Out of system resources.");
}
// Wait for the VM thread to become ready, and VMThread::run to initialize
// Monitors can have spurious returns, must always check another state flag
{
MutexLocker ml(Notify_lock);
os::start_thread(vmthread);
while (vmthread->active_handles() == NULL) {
Notify_lock->wait();
}
}
}
VMThread是一种特殊的jvm线程,用于执行比如GC等操作,java代码的Thread和JVM里面的JavaThread对应,这一点后续再研究;上面的代码片段首先关注【VMThread::create()】这个函数调用,在VMThread.cpp中实现了该函数:
void VMThread::create() {
assert(vm_thread() == NULL, "we can only allocate one VMThread");
_vm_thread = new VMThread();
// Create VM operation queue
_vm_queue = new VMOperationQueue();
guarantee(_vm_queue != NULL, "just checking");
_terminate_lock = new Monitor(Mutex::safepoint, "VMThread::_terminate_lock", true,
Monitor::_safepoint_check_never);
if (UsePerfData) {
// jvmstat performance counters
Thread* THREAD = Thread::current();
_perf_accumulated_vm_operation_time =
PerfDataManager::create_counter(SUN_THREADS, "vmOperationTime",
PerfData::U_Ticks, CHECK);
}
}
create函数在new了一个VMThread对象实例同时,为该VMThread创建了一个VMOperationQueue,VMThread有一个重要的成员叫_vm_queue,看看它的定义:
static VMOperationQueue* _vm_queue; // Queue (w/ policy) of VM operations
根据注释可以将该queue理解为是VMThread的任务队列,但是队列内部存放的任务都是VMOperation,不能是其他类型的任务,那VMOperation是什么呢?其实有一个基类叫VM_Operation,有一个子类叫VM_GC_Operation,就是专门来做GC的任务,在对象申请内存分配失败的时候会生成一个VM_CollectForAllocation任务来做GC,_vm_queue队列就是用来存储这些任务的,VMThread会不断来check该队列是否有任务需要执行,这种工作模式类似于特殊的线程池,这个线程池只有一个VMThread,_vm_queue就是线程池中的任务队列;
VMThread创建完成之后,create_vm函数将等到VMThread启动成功,判断VMThread是否已经正常工作的标准是vmthread->active_handles() == NULL为true,也就是执行了VMThread的run函数,这里说明一下,JVM的线程实现方法是将一个os线程绑定到JVM线程上,所以每创建一个JVM线程都需要创建一个os线程来做绑定,不同环境下创建os线程的方法不一样,比如在Mac下,就是使用bsd的方法来创建os线程的;回头看create_vm函数里面的那段代码片段,可以看到使用了os::create_thread(vmthread, os::vm_thread)来创建了一个os线程,在os_bsd.cpp内部的create_thread函数里面,可以看到创建了一个os线程,并且将该os线程绑定到了创建好的VMThread上,几乎在所有的os上创建线程的同时需要指定一个方法入口,使得os在创建好了线程之后可以准备执行相应的代码,可以在create_thread方法里面看到下面的代码片段:
pthread_t tid;
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
thread_native_entry就是上面提到的代码入口,可以在thread_native_entry函数内部看到执行了VMThread的run方法,到此create_vm函数可以继续执行;
接下来再回过头来看看VMThread的run函数,该函数执行了一些线程初始化的工作,比如设置线程名称,线程优先级等,然后执行了一个关键的方法:loop,该方法可以理解为VMThread将不断轮询来从自己的任务队列_vm_queue中获取任务来执行,下面来仔细研究一下loop函数的关键步骤。
- (1)、通过remove_next方法获取任务,如果当前任务队列中没有待执行的任务,那么remove_next函数会返回NULL,下面是remove_next函数的具体实现
VM_Operation* VMOperationQueue::remove_next() {
// Assuming VMOperation queue is two-level priority queue. If there are
// more than two priorities, we need a different scheduling algorithm.
assert(SafepointPriority == 0 && MediumPriority == 1 && nof_priorities == 2,
"current algorithm does not work");
// simple counter based scheduling to prevent starvation of lower priority
// queue. -- see 4390175
int high_prio, low_prio;
if (_queue_counter++ < 10) {
high_prio = SafepointPriority;
low_prio = MediumPriority;
} else {
_queue_counter = 0;
high_prio = MediumPriority;
low_prio = SafepointPriority;
}
return queue_remove_front(queue_empty(high_prio) ? low_prio : high_prio);
}
VM_Operation* VMOperationQueue::queue_remove_front(int prio) {
if (queue_empty(prio)) return NULL;
assert(_queue_length[prio] >= 0, "sanity check");
_queue_length[prio]--;
VM_Operation* r = _queue[prio]->next();
assert(r != _queue[prio], "cannot remove base element");
unlink(r);
return r;
}
- (2)、如果发现任务队列中没有待执行的任务,那么VMThread不能一直傻傻的轮询啊,就会让自己进入等待状态
- (3)、在(2)步骤中等待多时之后,VMThread可能会被一些任务填充线程唤醒(notify),这个时候loop函数就会继续执行接下来的代码,有一些Operation任务要求在safe_point执行,比如FullGC,使用SafepointSynchronize::begin()和SafepointSynchronize::end()可以达到这个目的,就像下面这样:
SafepointSynchronize::begin()
/// safe point code
SafepointSynchronize::end();
无论如何,接下来就是要执行队列中取出来的任务了,所以evaluate_operation(_cur_vm_operation)方法应该是我们接下来应该关注的;在evaluate_operation函数内部看到了调用了evaluate()函数,接着看看evaluate函数;
void VM_Operation::evaluate() {
ResourceMark rm;
outputStream* debugstream;
bool enabled = log_is_enabled(Debug, vmoperation);
if (enabled) {
debugstream = Log(vmoperation)::debug_stream();
debugstream->print("begin ");
print_on_error(debugstream);
debugstream->cr();
}
doit();
if (enabled) {
debugstream->print("end ");
print_on_error(debugstream);
debugstream->cr();
}
}
关键的是doit()函数,这里面就是具体的任务执行内容,不同的Operation的doit内容都是不一样的,就算是GC_Opertion,还是有多种不同的方式的,比如上面提到了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();
}
}
gch->satisfy_failed_allocation就是为了解决空间分配失败的,去看satisfy_failed_allocation函数的注释,可以看到:
// Callback from VM_GenCollectForAllocation operation.
// This function does everything necessary/possible to satisfy an
// allocation request that failed in the youngest generation that should
// have handled it (including collection, expansion, etc.)
HeapWord* satisfy_failed_allocation(size_t size, bool is_tlab);
这个函数会被VM_GenCollectForAllocation执行的时候回调,也就是doit函数执行的时候调用这个函数,这个函数会做类似于垃圾收集,堆扩展等工作来满足一个"allocation request";当然,回调这个函数之前必然已经尝试进行空间分配申请了,并且已经失败了,所以该函数需要极尽所能去做工作来腾出空间(申请新的空间)来满足已经失败的空间分配申请;collectorPolicy类实现了垃圾收集的策略,所谓垃圾收集策略就是应该在什么时候做GC,做什么类型的GC等,参考价值很大;下面可以试着来看一下satisfy_failed_allocation函数具体是怎么做的;
从上面这张图可以看到,如果发现gc_lock是活动的,也就说明已经有其他的线程触发了GC,那么这个时候策略就是扩展堆来满足内存申请。
看if条件,如果增量GC是安全的,那么就执行增量安全,所谓增量GC,就是按照从轻到重的程度来做垃圾回收,大概分这么几个级别,首先是进行一次MinorGC,其次是进行一次FullGC,最后是进行一次带soft reference清理的FullGC;上面的图片对应的是第一种情况,进行一次MinorGC,然后尝试申请空间,如果成功就打住了,否则就要进行一次清理soft reference的FullGC了,硕大soft reference,可以大概说一下,java中的引用分四个级别,strong reference > soft reference > weak reference > phantom reference;强度梯度下降,strong reference只要对象还在被引用就不会被回收,而soft reference就不一样了,JVM在尝试进行GC来解决内存不足的状况下,如果发现还是无法满足内存申请,那么就会将这部分引用类型的对象回收回来,所以,在使用soft reference的时候不应该强依赖于对象,因为不知道什么时候就被回收了,这种引用可以用在缓存的场景中;weak reference的强度比soft弱一些,它只能存活到下次GC发生,而phantom reference就更弱了,弱到你根本无法获取到一个phantom reference对象,它唯一的作用就是可以在发生GC的时候告诉你它已经被回收了;下面的代码展示了进行FullGC的两种情况:
** 总结一下再allocate fail的应对策略,首先判断是否有其他线程触发了GC操作,如果是的话则不会进行GC操作,而是尝试去扩展堆来解决allocate fail,否则判断是否可以进行增量GC,如果可以,那么执行一次MinorGC,否则执行一次不回收soft reference的FullGC,之后判断是否可以解决allocate fail了,如果可以了就到此打住,否则进行一次彻底的FullGC,也就是将soft reference也回收回来 **
其实在(3)的时候,漏掉一个细节,VMThread会将任务队列中填充好的任务都执行完成,才会继续执行接下来的代码;最后希望能看一下到底在什么地方会将任务填充到VMThread的任务队列中去;还是拿VM_GenCollectForAllocation来说,可以在collectorPolicy.cpp中的mem_allocate_work看到执行了类似下面的代码:
VM_GenCollectForAllocation op(size, is_tlab, gc_count_before);
VMThread::execute(&op);
然后又回来VMThread看execute方法,可以看到下面的细节:
到此,GC任务是怎么运行的大概梳理了一下,具体的GC细节还是需要再梳理。