HotSpot中执行引擎技术详解(四)——hotspot探测

基本理论

在前一篇文章里面简单提及了一些HotSpot中的“热点(hotspot)”探测,这篇文章就要来详细探讨一下在HotSpot里面使用的热点探测技术。众所周知的是,JVM在检测到一个热点的时候,就会启动JIT。这些热点将被编译为本地机器码,编译好的机器码将被放在methodOop里面的一个保留字段。下一次执行的时候,JVM检测到这个methodOop的保留字段不为null,就会将程序控制从解释执行转移到执行编译后的代码。

通常意义上,热点是指一个代码块,这块代码被频繁执行。在一些情况下,热点也会代指一条被频繁执行的路径。这对应于热点探测中收集的两种数据:

  1. 一个代码块被执行的频率;
  2. 基于控制流的代码执行路径分析:举个例子来说,对于一个条件分支,true和false的分支的执行情况是不同的,可能一条分支的执行频率非常高,而另一条分支只有在极其罕见的情况下才会被执行。从代码的意义上来说,这两条分支的代码可能在物理上解决,即它们属于同一个代码块。


    热点块和热点路径

如上图,左边的绿色块即是指热点代码块,右边的红色线条即是热点路径。

从某种意义上来说,基于控制流的代码路径分析会更加精确。可以断定的是,一个执行频繁的代码块,里面可能含有的某一段代码会处于“冷”路径上(如前面所说的另外一个罕见分支)。但是在一条“热”的路径上,所有的代码都会是“热”的,被频繁执行,虽然它们之间热度上会有区别。

HotSpot是基于块的,即当我们描述HotSpot的热点代码的时候,就是指一个代码块,如果直接理解为某一个方法,也不能算是错。

HotSpot里面,代码的执行会达到两个阈值。第一个就是熟知的触发JIT的阈值,这种时候编译都是以方法为单位的;第二个是触发栈上替换(on-stack replacement)的阈值。这个阈值可以被循环所触发。触发的代码也会被编译为本地机器码,但是其不再是以方法为单位的,它可以是一个方法内的某一段代码,比如说整个循环体。在此种情况下,JVM会维护一个字节码到编译后本地机器码的映射。读者可以参阅The Java HotSpot Server Compiler论文

热点的探测有两种基本手段。第一种是在执行的代码中间插入一些“探针”,或者说“桩”,这些探针或者桩就是一小段的代码。这些代码就是用于收集代码运行时候的信息;另外一种方式是采样,即在某个时间点收集寄存器和内存中的数据,最为常见的是收集PC中的数值,由此来断定分支跳转等信息。采样的方法,显然其开销会比较小,但是却无法收集精细的数据;而插桩则会带来更加大的开销,但是却可以精确控制所需要收集的数据。HotSpot采用的是插桩的方法。HotSpot主要收集两个指标:

  1. 方法计数,为每个方法分配一个调用计数器,它出现在方法入口;
  2. 循环计数,为每个循环分配一个计数器;

当一个方法的方法计数器或者循环计数器出发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更加详细的内容的时候,还会回到这个地方,讨论两者之间的合作。比如说方法计数的衰减问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容