1. sweeper线程创建
1.1 什么是sweeper线程
Threads::create_vm是虚拟机创生纪,其中就包括jit编译器线程和codecache sweeper线程的创建:
void CompileBroker::init_compiler_sweeper_threads() {
...
if (MethodFlushing) {
// Initialize the sweeper thread
Handle thread_oop = create_thread_oop("Sweeper thread", CHECK);
jobject thread_handle = JNIHandles::make_local(THREAD, thread_oop());
make_thread(thread_handle, NULL, NULL, CHECK);
}
}
这个sweeper thread就是专门用来清理codecache的线程,而codecache是用来存放代码片段的地方,比如jit compiler编译后的方法代码,虚拟机内部用到的一些stub和adapter代码片段。
回到上面的代码,make_thread将会创建一个线程,然后将线程的入口设置为NMethodSweeper::sweeper_loop方法:
void NMethodSweeper::sweeper_loop() {
bool timeout;
while (true) {
{
ThreadBlockInVM tbivm(JavaThread::current());
MutexLockerEx waiter(CodeCache_lock, Mutex::_no_safepoint_check_flag);
const long wait_time = 60*60*24 * 1000;
timeout = CodeCache_lock->wait(Mutex::_no_safepoint_check_flag, wait_time);
}
if (!timeout) {
possibly_sweep();
}
}
}
获取CodeCache_lock之后wait在这里,而一般wait阻塞之后的线程会因为各种原因唤醒,比如os导致的假醒,其他线程调用CodeCache_lock->notify(),又或者超时。这里的逻辑是,如果线程因为超时醒来,就不做sweep操作,否则清理codecache,而这个possibly_sweep就是sweeper线程的核心逻辑了。
换句话说,只有其他线程notify这个codecache sweeper线程醒来,codecache sweeper线程才可能清理codecache。
1.2 清理codecache的时机
那么,有哪些地方可能notify清理线程醒来呢?有两个地方,每次在codecache分配空间的时候会notify,还有一处是JVM的白盒测试可以强制notify清理线程。
// 白盒测试强制notify
WB_ENTRY(void, WB_ForceNMethodSweep(JNIEnv* env, jobject o))
NMethodSweeper::force_sweep();
WB_END
// codecache分配内存日常notify
CodeBlob* CodeCache::allocate(int size, int code_blob_type, int orig_code_blob_type) {
// Possibly wakes up the sweeper thread.
NMethodSweeper::notify(code_blob_type);
assert_locked_or_safepoint(CodeCache_lock);
...
}
所以正常情况下可能发生codecache清理的时刻就是每次向codecache申请空间的时候。
2. sweeper的核心逻辑
2.1 判断是否清理
说完了分配清理时机,接下来是清理逻辑,因为前面多次提到是可能清理,不是一定清理,从名字possibly_sweep也能看出来。注释已经总结了,有三种情况可能清理codecache:
- codecache要满了
- 从上传清理到现在有足够多的状态改变
- 很久没清理了
/**
* This function invokes the sweeper if at least one of the three conditions is met:
* (1) The code cache is getting full
* (2) There are sufficient state changes in/since the last sweep.
* (3) We have not been sweeping for 'some time'
*/
void NMethodSweeper::possibly_sweep() {
assert(JavaThread::current()->thread_state() == _thread_in_vm, "must run in vm mode");
if (!_should_sweep) {
const int time_since_last_sweep = _time_counter - _last_sweep;
const int max_wait_time = ReservedCodeCacheSize / (16 * M);
double wait_until_next_sweep = max_wait_time - time_since_last_sweep -
MAX2(CodeCache::reverse_free_ratio(CodeBlobType::MethodProfiled),
CodeCache::reverse_free_ratio(CodeBlobType::MethodNonProfiled));
assert(wait_until_next_sweep <= (double)max_wait_time, "Calculation of code cache sweeper interval is incorrect");
if ((wait_until_next_sweep <= 0.0) || !CompileBroker::should_compile_new_jobs()) {
_should_sweep = true;
}
}
// Remember if this was a forced sweep
bool forced = _force_sweep;
// Force stack scanning if there is only 10% free space in the code cache.
// We force stack scanning only if the non-profiled code heap gets full, since critical
// allocations go to the non-profiled heap and we must be make sure that there is
// enough space.
double free_percent = 1 / CodeCache::reverse_free_ratio(CodeBlobType::MethodNonProfiled) * 100;
if (free_percent <= StartAggressiveSweepingAt) {
do_stack_scanning();
}
if (_should_sweep || forced) {
init_sweeper_log();
sweep_code_cache();
}
// We are done with sweeping the code cache once.
_total_nof_code_cache_sweeps++;
_last_sweep = _time_counter;
// Reset flag; temporarily disables sweeper
_should_sweep = false;
// If there was enough state change, 'possibly_enable_sweeper()'
// sets '_should_sweep' to true
possibly_enable_sweeper();
// Reset _bytes_changed only if there was enough state change. _bytes_changed
// can further increase by calls to 'report_state_change'.
if (_should_sweep) {
_bytes_changed = 0;
}
if (forced) {
assert(_force_sweep, "Should be a forced sweep");
MutexLockerEx mu(CodeCache_lock, Mutex::_no_safepoint_check_flag);
_force_sweep = false;
CodeCache_lock->notify();
}
}
一个一个来说。
十七行的if里面的两个条件,前者wait_until_next_sweep对应(3),逻辑见代码,后面的should_compile_new_jobs对应codecache对应(1):
void CompileBroker::handle_full_code_cache(int code_blob_type) {
UseInterpreter = true;
if (UseCompiler || AlwaysCompileLoopMethods ) {
...
#ifndef PRODUCT
if (CompileTheWorld || ExitOnFullCodeCache) {
codecache_print(/* detailed= */ true);
before_exit(JavaThread::current());
exit_globals(); // will delete tty
vm_direct_exit(CompileTheWorld ? 0 : 1);
}
#endif
if (UseCodeCacheFlushing) {
// Since code cache is full, immediately stop new compiles
if (CompileBroker::set_should_compile_new_jobs(CompileBroker::stop_compilation)) {
NMethodSweeper::log_sweep("disable_compiler");
}
} else {
disable_compilation_forever();
}
CodeCache::report_codemem_full(code_blob_type, should_print_compiler_warning());
}
}
上面意思很清楚了,如果编译器发现codecache要满的时候,会用CompileBroker::set_should_compile_new_jobs设置不可编译热方法,然后等待sweeper线程清理,清理完后又用这个方法设置为可以继续编译热方法,这是后话。
非产品级虚拟机还可以使用-XX:+ExitOnFullCodeCache在codecache满的时候直接终止虚拟机执行,谁说一定要清理了?!
最后代码的46行possibly_enable_sweeper对应(2),他其实比较简单:
void NMethodSweeper::possibly_enable_sweeper() {
double percent_changed = ((double)_bytes_changed / (double)ReservedCodeCacheSize) * 100;
if (percent_changed > 1.0) {
_should_sweep = true;
}
}
这里的意思是,只要codecache里面1%的状态改变了,那么就可以清理。说了半天,到底什么是状态改变?
nmethod是虚拟机中用来表示编译后的java方法,他有很多状态:not_installed,in_use,zombie,not_entrant,unloaded。in_use表示这个java方法可以正常使用,not_entrant表示方法已经不可进入了,但还活着,可能还在栈上。最后zombie表示彻底死了,栈上也没了。unloaded表示nmethod已经清理完毕,不可使用。
举个例子,c2会使用激进的内联策略编译那些非final的方法。如果后来某个时间,有一个子类重写了这个方法,这个被编译的代码就不能再使用了。这个方法即nmethod被标注为make_not_entrant,表示后续的调用不能再用它,但是现存的调用还可以使用它。当现存的调用使用完毕返回后,栈帧中就不存在指向它的eip了,这个nmethod继而被标注为zombie,可以被清理了。
说了这么多,和sweeper相关的其实就一点,nmethod的状态变为not_entrant/zombie/unloaded时,上面的_bytes_changed就会增加,增加的数量是nmethod的大小,也就是java方法编译后的总大小。也就是说,如果一个方法有1M,codecache有5m,当方法被设置为unloaded的时候,相当于20%的状态改变了,此时可以发生清理。
总结一下,首先是codecache的分配会调用sweeper,然后三种情况可能清理,接着可选的扫描栈,最后清理,这是本文核心主线,后面几步全部位于possibly_sweep。
2.2 扫描栈
扫描栈位于possibly_sweep里面31行的do_stack_scanning()。如果codecache只有10%的空余空间了,就会发生这一步(由产品级参数-XX:StartAggressiveSweepingAt=10控制)
为什么说是可选呢??????
它会投递给虚拟机线程一个VM_MarkActiveNMethods,促使虚拟机线程进入安全点,然后执行NMethodSweeper::mark_active_nmethods
2.3 清理方法
清理方法位于possibly_sweep里面的36行的sweep_code_cache,它遍历所有nmethod,然后用process_compiled_method判断是否清理,代码相当直观:
NMethodSweeper::MethodStateChange NMethodSweeper::process_compiled_method(CompiledMethod* cm) {
...
if (cm->is_locked_by_vm()) {
// But still remember to clean-up inline caches for alive nmethods
if (cm->is_alive()) {
// Clean inline caches that point to zombie/non-entrant/unloaded nmethods
MutexLocker cl(CompiledIC_lock);
cm->cleanup_inline_caches();
SWEEP(cm);
}
return result;
}
if (cm->is_zombie()) {
assert(!cm->is_locked_by_vm(), "must not flush locked Compiled Methods");
cm->flush();
assert(result == None, "sanity");
result = Flushed;
} else if (cm->is_not_entrant()) {
// If there are no current activations of this method on the
// stack we can safely convert it to a zombie method
OrderAccess::loadload(); // _stack_traversal_mark and _state
if (cm->can_convert_to_zombie()) {
{
MutexLocker cl(CompiledIC_lock);
cm->clear_ic_callsites();
}
// Code cache state change is tracked in make_zombie()
cm->make_zombie();
SWEEP(cm);
if (cm->is_osr_method() && !cm->is_locked_by_vm()) {
assert(cm->is_zombie(), "nmethod must be unregistered");
cm->flush();
assert(result == None, "sanity");
result = Flushed;
} else {
assert(result == None, "sanity");
result = MadeZombie;
assert(cm->is_zombie(), "nmethod must be zombie");
}
} else {
// Still alive, clean up its inline caches
MutexLocker cl(CompiledIC_lock);
cm->cleanup_inline_caches();
SWEEP(cm);
}
} else if (cm->is_unloaded()) {
// Code is unloaded, so there are no activations on the stack.
// Convert the nmethod to zombie or flush it directly in the OSR case.
if (cm->is_osr_method()) {
SWEEP(cm);
// No inline caches will ever point to osr methods, so we can just remove it
cm->flush();
assert(result == None, "sanity");
result = Flushed;
} else {
// Code cache state change is tracked in make_zombie()
cm->make_zombie();
SWEEP(cm);
assert(result == None, "sanity");
result = MadeZombie;
}
} else {
if (cm->is_nmethod()) {
possibly_flush((nmethod*)cm);
}
// Clean inline caches that point to zombie/non-entrant/unloaded nmethods
MutexLocker cl(CompiledIC_lock);
cm->cleanup_inline_caches();
SWEEP(cm);
}
return result;
}
process_compiled_method就是根据方法的状态,判断是否可以清理。比如,如果是zombie状态,那么可以清理,如果是not_entrant,能转成zombie就清理,不能转就暂时只清理inline cache,等等。