一 为什么要延迟编译
在经济学里,有二八法则,说的是80%的财富集中在20%的人手里,然后这20%的人又服从二八法则,那另外80%的人也符合二八法则。
研究发现,在JVM的运行过程中,80%的处理器时间执行20%的代码,这个过程也可以递归。这20%的代码被称为热点hotspot,而被执行的方法同样被称为热点方法。所以只有将这20%的代码编译为本地代码才能达到最优的性能。因为大部分代码执行次数很少甚至只执行一次,所以没必要编译。因为解释一次,肯定比编译再执行一次更节省资源,也更快。只编译执行次数多的代码是JIT(Just-In-Time)的核心要义,因为JIT的意思就是及时。
从JAVA8开始,Oracle对Sun Hotspot虚拟机进行优化,开始集成JRocket VM中的JIT编译器。
大部分人都知道JIT分为Client Compiler和Server Compier。这两种都有不同的实现,简要下如下表:
编译器 | 实现 |
Client Compiler | C1 Compiler |
Server Compier | C2 Compiler |
Graal Compiler | |
Shark Compiler(已经废弃) |
二 如何检测热点代码
知道了只编译热点代码后,马上要想的是如何实现。JVM使用了两个计数器来实现。一个是方法调用计数器invocation counter,一个是回边计数器backedge_counter。我为什么敢这么说呢?这年头不翻源码都不好意思写文章了,哈哈,必须讲证据,请看源码hotspot/src/share/vm/oops/methodCounters.hpp
class MethodCounters: public MetaspaceObj {
friend class VMStructs;
private:
int _interpreter_invocation_count; // Count of times invoked (reused as prev_event_count in tiered)
u2 _interpreter_throwout_count; // Count of times method was exited via exception while interpreting
u2 _number_of_breakpoints; // fullspeed debugging support
InvocationCounter _invocation_counter; // Incremented before each activation of the method - used to trigger frequency-based optimizations
InvocationCounter _backedge_counter; // Incremented before each backedge taken - used to trigger frequencey-based optimizations
这里我再讲下回边,回边是图论/图算法中的概念,回边是图中DFS搜索中发现的指向前驱节点的边,也就是形成环的那条边。把程序的执行看成图,回边就代表了回到循环起点的最后一个步骤。可以这么说回边计数器就是循环计数器。回边的检测是用宏定义写的一个宏函数,源码hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp:
#define DO_BACKEDGE_CHECKS(skip, branch_pc) \
if ((skip) <= 0) { \
MethodCounters* mcs; \
GET_METHOD_COUNTERS(mcs); \
if (UseLoopCounter) { \
bool do_OSR = UseOnStackReplacement; \
mcs->backedge_counter()->increment(); \
if (do_OSR) do_OSR = mcs->backedge_counter()->reached_InvocationLimit(); \
if (do_OSR) { \
nmethod* osr_nmethod; \
OSR_REQUEST(osr_nmethod, branch_pc); \
if (osr_nmethod != NULL && osr_nmethod->osr_entry_bci() != InvalidOSREntryBci) { \
intptr_t* buf = SharedRuntime::OSR_migration_begin(THREAD); \
istate->set_msg(do_osr); \
istate->set_osr_buf((address)buf); \
istate->set_osr_entry(osr_nmethod->osr_entry()); \
return; \
} \
} \
} /* UseCompiler ... */ \
mcs->invocation_counter()->increment(); \
SAFEPOINT; \
}
可以从上面的代码里看到,里面还调用了方法调用计数器。然后在比较运算符和goto\gotow指令的实现中,使回边计算器递增。比如实现==与!=的宏函数:
#define COMPARISON_OP2(name, comparison) \
COMPARISON_OP(name, comparison) \
CASE(_if_acmp##name): { \
int skip = (STACK_OBJECT(-2) comparison STACK_OBJECT(-1)) \
? (int16_t)Bytes::get_Java_u2(pc + 1) : 3; \
address branch_pc = pc; \
UPDATE_PC_AND_TOS(skip, -2); \
DO_BACKEDGE_CHECKS(skip, branch_pc); \
CONTINUE; \
}
方法调用计数器除了上面说的回边计数器里调用了以外,还在另一个地方调用了,就是方法入口method entry。源码还是源码hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp:
case method_entry: {
THREAD->set_do_not_unlock();
// count invocations
assert(initialized, "Interpreter not initialized");
if (_compiling) {
MethodCounters* mcs;
GET_METHOD_COUNTERS(mcs);
if (ProfileInterpreter) {
METHOD->increment_interpreter_invocation_count(THREAD);
}
mcs->invocation_counter()->increment();
if (mcs->invocation_counter()->reached_InvocationLimit()) {
CALL_VM((void)InterpreterRuntime::frequency_counter_overflow(THREAD, NULL), handle_exception);
// We no longer retry on a counter overflow
// istate->set_msg(retry_method);
// THREAD->clr_do_not_unlock();
// return;
}
SAFEPOINT;
}
if ((istate->_stack_base - istate->_stack_limit) != istate->method()->max_stack() + 1) {
// initialize
os::breakpoint();
}
三 怎么衡量热点代码
编译器 | 调用次数 | 回边次数 |
---|---|---|
C1 | 1500 | 100000 |
C2 | 10000 | 100000 |
我是讲究证据的,哈哈,死命找源码。
在JVM源码中hotspot/src/cpu/x86/vm/c1_globals_x86.hpp有这么一段:
define_pd_global(intx, CompileThreshold, 1500 );
define_pd_global(intx, BackEdgeThreshold, 100000);
在JVM源码中hotspot/src/cpu/x86/vm/c2_globals_x86.hpp有这么一段:
define_pd_global(intx, CompileThreshold, 10000);
define_pd_global(intx, BackEdgeThreshold, 100000);