一个方法被HotSpot JIT编译了,那么调用它的方法们需要编译才回用新的版本么?

有一个私有方法B,也就被A调用。假如B太大了并没有被A inline,那么B之后被JIT重新编译了,A需要重新被JIT编译,才会pickup新版本么?哦,我觉得是不会,因为要不就编译个没完了。而且log search也没有找到这样的例子。应该就是一个指针。那么如果是inline了的B呢?(逃想想好像也不要。如果需要的话,那么一些小的很常用的方法一旦被recompile了,必然导致山崩式recomple。所以(突然觉得这个问题好愚蠢(
关注者
18
被浏览
1989
题主设定的场景:
一个方法被HotSpot JIT编译了,那么调用它的方法们需要编译才回用新的版本么?

有一个私有方法B,也就被A调用。假如B太大了并没有被A inline,那么B之后被JIT重新编译了,A需要重新被JIT编译,才会pickup新版本么?
不需要。假如是A先被JIT编译并且没有内联对B的调用,后来B也被JIT编译了。那么之后在下次A调用B的时候就会感知B被JIT编译了并改为调用JIT编译版的B方法。这里的关键词是:code patching / self-modifying code,并且是lazy的。

HotSpot VM里有相当多通过code patching方式实现的功能。

当JIT编译器编译一个方法,发现里面有些对别的方法对调用点(call site),而它们的目标是“可静态决定”的(可能是static或者private方法,也可能是虚方法但通过一些额外信息可以知道这个虚方法只有一个实现版本),但却又出于某些原因而未能内联这些目标方法的话,就会生成出一种叫做 CompiledStaticCall 的结构作为调用点的实现。

src/share/vm/code/compiledIC.hpp
//-----------------------------------------------------------------------------
// The CompiledStaticCall represents a call to a static method in the compiled
//
// Transition diagram of a static call site is somewhat simpler than for an inlined cache:
//
//
//           -----<----- Clean ----->-----
//          /                             \
//         /                               \
//    compilled code <------------> interpreted code
//
//  Clean:            Calls directly to runtime method for fixup
//  Compiled code:    Calls directly to compiled code
//  Interpreted code: Calls to stub that set Method* reference
关于 CompiledStaticCall 是什么东西,先放个传送门:Overview of CompiledIC and CompiledStaticCall - HotSpot Internals - OpenJDK Wiki

简单来说,CompiledStaticCall 是一个可以被patch的调用点。它对应的机器码其实就是条普通的call指令——但这条call指令可以被patch成调用不同的目标。

JIT编译器在生成代码的时候,生成的 CompiledStaticCall 是处于clean状态的。此时它的目标是 SharedRuntime::get_resolve_static_call_stub() 返回的那个stub。
首次执行到这个调用点的时候会调用这个stub(注意这个动作是lazy的),进到runtime里的函数去查找指定的Method当前的 from_compiled_entry() 指向哪里,并把 from_compiled_entry() 的值patch到那条call指令上。
// resolve a static call and patch code
JRT_BLOCK_ENTRY(address, SharedRuntime::resolve_static_call_C(JavaThread *thread ))
  methodHandle callee_method;
  JRT_BLOCK
    callee_method = SharedRuntime::resolve_helper(thread, false, false, CHECK_NULL);
    thread->set_vm_result_2(callee_method());
  JRT_BLOCK_END
  // return compiled code entry point after potential safepoints
  assert(callee_method->verified_code_entry() != NULL, " Jump to zero!");
  return callee_method->verified_code_entry();
JRT_END

// Resolve a virtual call that can be statically bound (e.g., always
// monomorphic, so it has no inline cache).  Patch code to resolved target.
JRT_BLOCK_ENTRY(address, SharedRuntime::resolve_opt_virtual_call_C(JavaThread *thread))
  methodHandle callee_method;
  JRT_BLOCK
    callee_method = SharedRuntime::resolve_helper(thread, true, true, CHECK_NULL);
    thread->set_vm_result_2(callee_method());
  JRT_BLOCK_END
  // return compiled code entry point after potential safepoints
  assert(callee_method->verified_code_entry() != NULL, " Jump to zero!");
  return callee_method->verified_code_entry();
JRT_END
假如目标方法当前尚未被JIT编译,则它的 from_compiled_entry() 会得到一个 c2i adapter stub 的地址(compiled-to-interpreted),以此跳进解释器去执行目标方法。

当目标方法被JIT编译后,它的 from_compiled_entry() 会被新设置为JIT编译后的代码的入口地址,而 c2i adapter 里有对应的检查看目标方法现在是不是已经有JIT编译的代码了,如果有的话就再 resolve and patch 一次,这样就可以lazy地发现目标方法有被编译并且将caller里的调用点给patch到新的目标地址上。

而目标方法在被JIT编译后还可能会被deoptimize退回到解释器去执行,在tiered compilation环境中也可能被更高层的编译模式所编译,这些都需要切换调用目标的实际入口地址,而它们也是跟上述流程相似通过 resolve and patch 方式来实现的。

例如说一个被JIT编译后的方法被deoptimize的时候,它的入口地址处的代码会被patch成一个 jmp SharedRuntime::get_handle_wrong_method_stub() 。
这样这个方法再被调用的时候就自然会进runtime去重新 resolve and patch ,得到当前新的 from_compiled_entry() 并且把结果patch到caller那边去——此时 from_compiled_entry() 会重新指向 c2i adapter 于是就又回解释器去跑了。