Runtime CodeCache Sweeper

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:

  1. codecache要满了
  2. 从上传清理到现在有足够多的状态改变
  3. 很久没清理了
/**
 * 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,等等。

原文地址:https://www.cnblogs.com/kelthuzadx/p/15726506.html