基本理论
在前一篇文章里面简单提及了一些HotSpot中的“热点(hotspot)”探测,这篇文章就要来详细探讨一下在HotSpot里面使用的热点探测技术。众所周知的是,JVM在检测到一个热点的时候,就会启动JIT。这些热点将被编译为本地机器码,编译好的机器码将被放在methodOop里面的一个保留字段。下一次执行的时候,JVM检测到这个methodOop的保留字段不为null,就会将程序控制从解释执行转移到执行编译后的代码。
通常意义上,热点是指一个代码块,这块代码被频繁执行。在一些情况下,热点也会代指一条被频繁执行的路径。这对应于热点探测中收集的两种数据:
- 一个代码块被执行的频率;
-
基于控制流的代码执行路径分析:举个例子来说,对于一个条件分支,true和false的分支的执行情况是不同的,可能一条分支的执行频率非常高,而另一条分支只有在极其罕见的情况下才会被执行。从代码的意义上来说,这两条分支的代码可能在物理上解决,即它们属于同一个代码块。
如上图,左边的绿色块即是指热点代码块,右边的红色线条即是热点路径。
从某种意义上来说,基于控制流的代码路径分析会更加精确。可以断定的是,一个执行频繁的代码块,里面可能含有的某一段代码会处于“冷”路径上(如前面所说的另外一个罕见分支)。但是在一条“热”的路径上,所有的代码都会是“热”的,被频繁执行,虽然它们之间热度上会有区别。
HotSpot是基于块的,即当我们描述HotSpot的热点代码的时候,就是指一个代码块,如果直接理解为某一个方法,也不能算是错。
HotSpot里面,代码的执行会达到两个阈值。第一个就是熟知的触发JIT的阈值,这种时候编译都是以方法为单位的;第二个是触发栈上替换(on-stack replacement)的阈值。这个阈值可以被循环所触发。触发的代码也会被编译为本地机器码,但是其不再是以方法为单位的,它可以是一个方法内的某一段代码,比如说整个循环体。在此种情况下,JVM会维护一个字节码到编译后本地机器码的映射。读者可以参阅The Java HotSpot Server Compiler论文
热点的探测有两种基本手段。第一种是在执行的代码中间插入一些“探针”,或者说“桩”,这些探针或者桩就是一小段的代码。这些代码就是用于收集代码运行时候的信息;另外一种方式是采样,即在某个时间点收集寄存器和内存中的数据,最为常见的是收集PC中的数值,由此来断定分支跳转等信息。采样的方法,显然其开销会比较小,但是却无法收集精细的数据;而插桩则会带来更加大的开销,但是却可以精确控制所需要收集的数据。HotSpot采用的是插桩的方法。HotSpot主要收集两个指标:
- 方法计数,为每个方法分配一个调用计数器,它出现在方法入口;
- 循环计数,为每个循环分配一个计数器;
当一个方法的方法计数器或者循环计数器出发JIT阈值,就会被认为是一个热点,即会被编译为本地代码。
HotSpot实现
现在深入到HotSpot里面的去看一看它是如何插桩的。这里主要以方法调用的插桩为例。方法调用的字节码指令有invokestatic, invokevirtual, invokeinterface等,这里我将用Invokevirtual指令来作为例子。
现在的HotSpot默认采用的是模板解释器 ,该系列的第二篇已经对此有解释了。所以进去模板解释器的templateTable里面找到该指令的模板生成器——实际上就是一个方法,其定义是:
// src/share/vm/interpreter/templateTable.hpp
static void invokevirtual(int byte_no);
该方法实现是与CPU架构直接相关的,x86 64的实现是:
// src/cpu/x86/vm/templateTable_x86_64.cpp
void TemplateTable::invokevirtual(int byte_no) {
transition(vtos, vtos);
assert(byte_no == f2_byte, "use this argument");
prepare_invoke(byte_no,
rbx, // method or vtable index
noreg, // unused itable index
rcx, rdx); // recv, flags
// rbx: index
// rcx: receiver
// rdx: flags
invokevirtual_helper(rbx, rcx, rdx);
}
调用计数是在invokevirtual_helper
方法里面被调用,其内调用了一个profile_virtual_call
方法,在其内完成了方法计数的增加:
void InterpreterMacroAssembler::profile_virtual_call(Register receiver,
Register mdp,
Register reg2,
bool receiver_can_be_null) {
if (ProfileInterpreter) {
// ...
if (receiver_can_be_null) {
// ...
// We are making a call. Increment the count for null receiver.
increment_mdp_data_at(mdp, in_bytes(CounterData::count_offset()));
//...
}
//...
}
}
现在知道在哪里方法计数被增加了,那么还有一个问题没有解决,即HotSpot将这些计数保存在哪里?一个很自然的想法就是保存在methodOop里面。注意的是,从前面贴出来的代码上并不能看出什么来,因为它增加方法计数是直接增加了寄存器中的数值。
methodOop里面有一段注释,已经解释了方法计数被放在哪里:
methodCounters对应于methodOop中声明的_method_counters字段,该字段是MethodCounters指针。而MethodCounters的定义如下:
// src/share/vn/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
//...
}
从其继承关系上也可以看出来,它被放在metaspace里面。它里面含有好几个计数器,之前一直追溯的就是_interpreter_invocation_count
计数,这个计数统计的就是整个方法被解释器调用的次数。除此以外,还有两个计数器可以关注一下:
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
这两个计数都是用于基于频率的优化。所谓基于频率的优化,其实也很好理解。显然,一个方法的调用次数并不能完全决定它是否是热点。比如说一个方法在第一分钟内调用了一万次,而后再没有被调用,而第二个方法虽然每分钟只被调用一千次,但是一直被调用。那么显然,第一个方法在第二分钟起,就不应该被认为是一个热点了,相反,第二个方法可能会一直被认为是一个热点。
InvocationCounter是一个很关键的类,它里面维护着如何断定一个方法是否属于热点——即是否触发阈值的逻辑,其定义在:
// src/share/vm/interpreter/invocationCounter.hpp
// InvocationCounters are used to trigger actions when a limit (threshold) is reached.
// For different states, different limits and actions can be defined in the initialization
// routine of InvocationCounters.
// Implementation notes: For space reasons, state & counter are both encoded in one word,
// The state is encoded using some of the least significant bits, the counter is using the
// more significant bits. The counter is incremented before a method is activated and an
// action is triggered when when count() > limit().
class InvocationCounter VALUE_OBJ_CLASS_SPEC {
private: // bit no: |31 3| 2 | 1 0 |
unsigned int _counter; // format: [count|carry|state]
// ...
bool reached_InvocationLimit(InvocationCounter *back_edge_count) const {
return (_counter & count_mask) + (back_edge_count->_counter & count_mask) >=
(unsigned int) InterpreterInvocationLimit;
}
bool reached_BackwardBranchLimit(InvocationCounter *back_edge_count) const {
return (_counter & count_mask) + (back_edge_count->_counter & count_mask) >=
(unsigned int) InterpreterBackwardBranchLimit;
}
// Do this just like asm interpreter does for max speed.
bool reached_ProfileLimit(InvocationCounter *back_edge_count) const {
return (_counter & count_mask) + (back_edge_count->_counter & count_mask) >=
(unsigned int) InterpreterProfileLimit;
}
// ...
}
到这里,我们就已经清楚,HotSpot是如何完成热点探测这一件事情的了。热点探测是和JIT息息相关的一种东西,等后续谈到JIT更加详细的内容的时候,还会回到这个地方,讨论两者之间的合作。比如说方法计数的衰减问题。