第55篇-回边计数

语言: CN / TW / HK

在前面介绍控制转移指令时只简单介绍了相关字节码解释执行的主要逻辑,没有介绍过统计相关的逻辑。对于控制转移指令来说,通常会调用TemplateTable::branch(bool is_jsr, bool is_wide)函数生成相关的汇编代码,这些汇编代码会含有统计的逻辑,这一篇将详细介绍。

控制转移指令中,大部分都会调用TemplateTable::branch()函数生成统计相关的代码,如下表所示。

Opcode

助记符

描述

生成统计相关代码的函数

0x99

ifeq

当栈顶 int型数值等于 0时跳转

TemplateTable::branch(false,false)

0x9a

ifne

当栈顶 int型数值不等于 0时跳转

TemplateTable::branch(false,false)

0x9b

iflt

当栈顶 int型数值小于 0时跳转

TemplateTable::branch(false,false)

0x9c

ifge

当栈顶 int型数值大于等于 0时跳转

TemplateTable::branch(false,false)

0x9d

ifgt

当栈顶 int型数值大于 0时跳转

TemplateTable::branch(false,false)

0x9e

ifle

当栈顶 int型数值小于等于 0时跳转

TemplateTable::branch(false,false)

0x9f

if_icmpeq

比较栈顶两 int型数值大小,当结果等于 0时跳转

TemplateTable::branch(false,false)

0xa0

if_icmpne

比较栈顶两 int型数值大小,当结果不等于 0时跳转

TemplateTable::branch(false,false)

0xa1

if_icmplt

比较栈顶两 int型数值大小,当结果小于 0时跳转

TemplateTable::branch(false,false)

0xa2

if_icmpge

比较栈顶两 int型数值大小,当结果大于等于 0时跳转

TemplateTable::branch(false,false)

0xa3

if_icmpgt

比较栈顶两 int型数值大小,当结果大于 0时跳转

TemplateTable::branch(false,false)

0xa4

if_icmple

比较栈顶两 int型数值大小,当结果小于等于 0时跳转

TemplateTable::branch(false,false)

0xa5

if_acmpeq

比较栈顶两引用型数值,当结果相等时跳转

TemplateTable::branch(false,false)

0xa6

if_acmpne

比较栈顶两引用型数值,当结果不相等时跳转

TemplateTable::branch(false,false)

0xa7

goto

无条件跳转

TemplateTable::branch(false,false)

0xa8

jsr

跳转至指定 16位 offset位置,并将 jsr下一条指令地址压入栈顶

TemplateTable::branch(true,false)

0xa9

ret

返回至本地变量指令的index的指令位置(一般与jsr或jsr_w联合使用)

InterpreterMacroAssembler::profile_ret()

0xaa

tableswitch

用于 switch条件跳转, case值连续(可变长度指令)

InterpreterMacroAssembler::profile_switch_case()

InterpreterMacroAssembler::profile_switch_default()

0xab

lookupswitch

用于 switch条件跳转, case值不连续(可变长度指令),会在字节码重写阶段改写为_fast_linearswitch或_fast_binaryswitch虚拟机内部使用的指令,所以字节码的逻辑要看这2个指令的逻辑

InterpreterMacroAssembler::profile_switch_case()

InterpreterMacroAssembler::profile_switch_default()

0xc8

goto_w

无条件跳转(宽索引)

TemplateTable::branch(false,true)

0xc9

jsr_w

跳转至指定 32位 offset位置,并将 jsr_w下一条指令地址压入栈顶

TemplateTable::branch(true,true)

下面以goto指令为例介绍。在之前写过的一篇 控制与转移字节码指令 中介绍过goto字节码指令,对应的生成函数为TemplateTable::_goto(),生成的汇编代码如下:

// 将当前栈帧中保存的Method*拷贝到%rcx中
0x00007fffe101dd10: mov    -0x18(%rbp),%rcx

// 如果开启了选项ProfileInterpreter,则执行分支跳转相关的性能统计
// %rax中保存着MDP(Method Data Pointer)
0x00007fffe101dd14: mov    -0x20(%rbp),%rax
// 如果Method::_method_data的值为NULL,则跳转到---- profile_continue ----
0x00007fffe101dd18: test   %rax,%rax
0x00007fffe101dd1b: je     0x00007fffe101dd39


// 代码执行到这里时,表示Method::_method_data的值不为NULL

// 根据Method::_method_data获取到JumpData::taken_off_set偏移处属性的值并存储到%rbx中
0x00007fffe101dd21: mov    0x8(%rax),%rbx
// 增加DataLayout::counter_increment,值为1
0x00007fffe101dd25: add    $0x1,%rbx
// sbb是带借位减法指令
0x00007fffe101dd29: sbb    $0x0,%rbx
// 存储回JumpData::taken_off_set偏移处
0x00007fffe101dd2d: mov    %rbx,0x8(%rax)


// The method data pointer needs to be updated to reflect the new target.
// %rax中存储的是MethodData
// 根据MethodData获取JumpData::displacement_off_set偏移处的值
0x00007fffe101dd31: add    0x10(%rax),%rax
// 将%rax中存储的值更新到栈中interpreter_frame_mdx_offset偏向处
0x00007fffe101dd35: mov    %rax,-0x20(%rbp)

Method::_method_data属性的类型为MethodData*,MethodData类中的_data属性能够保存Java方法的详细信息。例如一个Java方法的字节码指令可能有多个回边,那么这些回边相关的运行时信息都会存储到_data属性指向的一片内存区域中。如上的JumpData::taken_off_set就是在MethodData::_data指向的一片内存区域的相对应位置存储跳转的次数,另外还需要注意栈中的interpreter_frame_mdx_offset处存储的是method data pointer,这些东西还需要介绍了MethodData::_data数据存储结构以及相关存储内容后才能理解,下一篇将详细介绍。

接下来生成如下的汇编指令,如下:

// **** profile_continue ****

// 将当前字节码位置往后偏移1字节处开始的2字节数据读取到%rdx中

0x00007fffe101dd39: movswl 0x1(%r13),%edx
// 将%rdx中的值字节次序变反
0x00007fffe101dd3e: bswap  %edx
// 将%rdx中的值右移16位,上述两步就是为了计算跳转分支的偏移量
0x00007fffe101dd40: sar    $0x10,%edx
// 将%rdx中的数据从2字节扩展成4字节
0x00007fffe101dd43: movslq %edx,%rdx
// 将当前字节码地址加上%rdx保存的偏移量,计算跳转的目标地址
0x00007fffe101dd46: add    %rdx,%r13

如果UseLoopCounter为true时才会有如下汇编,在执行如下汇编时,各个寄存器的状态如下:

increment backedge counter for backward branches

rax: MDO
ebx: MDO bumped taken-count
rcx: method
rdx: target offset
r13: target bcp
r14: locals pointer

汇编如下:

// 校验rdx是否大于0,如果大于0说明是往前跳转,如果小于0说明是往后跳转,
// 如果大于0则跳转到---- dispatch ----,这样只有回边才会进行统计
0x00007fffe101dd49: test   %edx,%edx
0x00007fffe101dd4b: jns    0x00007fffe101de30 

// 执行这里时,说明有回边需要统计
// 检查Method::_method_counters是否为NULL,如果非空则跳转到---- has_counters ----
0x00007fffe101dd51: mov    0x20(%rcx),%rax
0x00007fffe101dd55: test   %rax,%rax
0x00007fffe101dd58: jne    0x00007fffe101ddf4

// 如果为空,则通过InterpreterRuntime::build_method_counters()函数创建一个新的MethodCounters
0x00007fffe101dd5e: push   %rdx
0x00007fffe101dd5f: push   %rcx
0x00007fffe101dd60: callq  0x00007fffe101dd6a
0x00007fffe101dd65: jmpq   0x00007fffe101dde8
0x00007fffe101dd6a: mov    %rcx,%rsi
0x00007fffe101dd6d: lea    0x8(%rsp),%rax
0x00007fffe101dd72: mov    %r13,-0x38(%rbp)
0x00007fffe101dd76: mov    %r15,%rdi
0x00007fffe101dd79: mov    %rbp,0x200(%r15)
0x00007fffe101dd80: mov    %rax,0x1f0(%r15)
0x00007fffe101dd87: test   $0xf,%esp
0x00007fffe101dd8d: je     0x00007fffe101dda5
0x00007fffe101dd93: sub    $0x8,%rsp
0x00007fffe101dd97: callq  0x00007ffff66b581c
0x00007fffe101dd9c: add    $0x8,%rsp
0x00007fffe101dda0: jmpq   0x00007fffe101ddaa
0x00007fffe101dda5: callq  0x00007ffff66b581c
0x00007fffe101ddaa: movabs $0x0,%r10
0x00007fffe101ddb4: mov    %r10,0x1f0(%r15)
0x00007fffe101ddbb: movabs $0x0,%r10
0x00007fffe101ddc5: mov    %r10,0x200(%r15)
0x00007fffe101ddcc: cmpq   $0x0,0x8(%r15)
0x00007fffe101ddd4: je     0x00007fffe101dddf
0x00007fffe101ddda: jmpq   0x00007fffe1000420
0x00007fffe101dddf: mov    -0x38(%rbp),%r13
0x00007fffe101dde3: mov    -0x30(%rbp),%r14
0x00007fffe101dde7: retq   


0x00007fffe101dde8: pop    %rcx
0x00007fffe101dde9: pop    %rdx
// 将创建出新的MethodCounters存储到%rax中
0x00007fffe101ddea: mov    0x20(%rcx),%rax
//如果创建失败,则跳转到到---- dispatch ----
0x00007fffe101ddee: je     0x00007fffe101de30

如下在开启-XX:+TieredCompilation选项的情况下,也就是开启分层编译时才会生成的汇编:

// **** has_counters ****

// 开启ProfileInterpreter性能收集才会生成的汇编

// 获取Method::_method_data属性到rbx中,并校验其是否为空,如果为空则跳转到 ---- no_mdo ----
0x00007fffe101ddf4: mov    0x18(%rcx),%rbx
0x00007fffe101ddf8: test   %rbx,%rbx
0x00007fffe101ddfb: je     0x00007fffe101de17

//Method::_method_data属性不为空,则增加Method::_method_data::_backedge_counter
// 计数值,如果超过阈值则跳转到---- backedge_counter_overflow ----
0x00007fffe101ddfd: mov    0x70(%rbx),%eax
0x00007fffe101de00: add    $0x8,%eax
0x00007fffe101de03: mov    %eax,0x70(%rbx)
0x00007fffe101de06: and    $0x1ff8,%eax
0x00007fffe101de0c: je     0x00007fffe101df22 
// 当没有超过阈值时,跳转到---- dispatch ----
0x00007fffe101de12: jmpq   0x00007fffe101de30

// **** no_mdo ****

// 增加Method::_method_counters::backedge_counter的调用计数,
// 如果超过阈值则跳转到---- backedge_counter_overflow ----
0x00007fffe101de17: mov    0x20(%rcx),%rcx
0x00007fffe101de1b: mov    0xc(%rcx),%eax
0x00007fffe101de1e: add    $0x8,%eax
0x00007fffe101de21: mov    %eax,0xc(%rcx)
0x00007fffe101de24: and    $0x1ff8,%eax
0x00007fffe101de2a: je     0x00007fffe101df22 


// **** dispatch ****

// r13已经变成目标跳转地址,这里是加载跳转地址的第一个字节码到rbx中,然后执行
// 字节码指令的跳转逻辑
0x00007fffe101de30: movzbl 0x0(%r13),%ebx
0x00007fffe101de35: movabs $0x7ffff73b9e40,%r10
0x00007fffe101de3f: jmpq   *(%r10,%rbx,8)


// **** profile_method ****
// 由于讨论的是分层编译情况下的汇编代码,所以并不会执行profile_method下面的汇编代码,
// 也就是不会调用profile_method()函数创建MethodData实例并赋值给Method::_method_data

// 通过call_VM()函数来调用InterpreterRuntime::profile_method()函数
0x00007fffe101de43: callq  0x00007fffe101de4d
0x00007fffe101de48: jmpq   0x00007fffe101dec8
0x00007fffe101de4d: lea    0x8(%rsp),%rax
0x00007fffe101de52: mov    %r13,-0x38(%rbp)
0x00007fffe101de56: mov    %r15,%rdi
0x00007fffe101de59: mov    %rbp,0x200(%r15)
0x00007fffe101de60: mov    %rax,0x1f0(%r15)
0x00007fffe101de67: test   $0xf,%esp
0x00007fffe101de6d: je     0x00007fffe101de85
0x00007fffe101de73: sub    $0x8,%rsp
0x00007fffe101de77: callq  0x00007ffff66b4d84
0x00007fffe101de7c: add    $0x8,%rsp
0x00007fffe101de80: jmpq   0x00007fffe101de8a
0x00007fffe101de85: callq  0x00007ffff66b4d84
0x00007fffe101de8a: movabs $0x0,%r10
0x00007fffe101de94: mov    %r10,0x1f0(%r15)
0x00007fffe101de9b: movabs $0x0,%r10
0x00007fffe101dea5: mov    %r10,0x200(%r15)
0x00007fffe101deac: cmpq   $0x0,0x8(%r15)
0x00007fffe101deb4: je     0x00007fffe101debf
0x00007fffe101deba: jmpq   0x00007fffe1000420
0x00007fffe101debf: mov    -0x38(%rbp),%r13
0x00007fffe101dec3: mov    -0x30(%rbp),%r14
0x00007fffe101dec7: retq   
// 结束call_VM()函数结束


// restore target bytecode
0x00007fffe101dec8: movzbl 0x0(%r13),%ebx
// 调用set_method_data_pointer_for_bcp()函数生成的汇编
0x00007fffe101decd: push   %rax
0x00007fffe101dece: push   %rbx

// 获取Method::_method_data并存储到%rax中
0x00007fffe101decf: mov    -0x18(%rbp),%rbx
0x00007fffe101ded3: mov    0x18(%rbx),%rax
// 如果Method::_method_data为NULL,则跳转到---- set_mdp ----
0x00007fffe101ded7: test   %rax,%rax
0x00007fffe101deda: je     0x00007fffe101df17

// 通过call_VM_leaf()函数生成的汇编调用InterpreterRuntime::bcp_to_di()函数
0x00007fffe101dee0: mov    %r13,%rsi
0x00007fffe101dee3: mov    %rbx,%rdi
0x00007fffe101dee6: test   $0xf,%esp
0x00007fffe101deec: je     0x00007fffe101df04
0x00007fffe101def2: sub    $0x8,%rsp
0x00007fffe101def6: callq  0x00007ffff66b4bb4
0x00007fffe101defb: add    $0x8,%rsp
0x00007fffe101deff: jmpq   0x00007fffe101df09
0x00007fffe101df04: callq  0x00007ffff66b4bb4

// rax: mdi
// mdo is guaranteed to be non-zero here, we checked for it before the call.

// 将Method::_method_data存储到%rbx中
0x00007fffe101df09: mov    0x18(%rbx),%rbx
// 增加Method::_method_data::_data偏移
0x00007fffe101df0d: add    $0x90,%rbx
0x00007fffe101df14: add    %rbx,%rax

// **** set_mdp ****

// 通过interpreter_frame_mdx_offset来获取mdx
0x00007fffe101df17: mov    %rax,-0x20(%rbp)
0x00007fffe101df1b: pop    %rbx
0x00007fffe101df1c: pop    %rax
// 结束set_method_data_pointer_for_bcp()函数调用

// 跳转到---- dispatch ----
0x00007fffe101df1d: jmpq   0x00007fffe101de30

调用的InterpreterRuntime::profile_method()函数的实现如下:

IRT_ENTRY(void, InterpreterRuntime::profile_method(JavaThread* thread))
  // ..

  frame  fr = thread->last_frame();
  methodHandle  method(thread, fr.interpreter_frame_method());
  Method::build_interpreter_method_data(method, THREAD);
IRT_END

// 如果Method::MethodData的值为NULL,则创建一个新的MethodData实例,然后赋值
void Method::build_interpreter_method_data(methodHandle method, TRAPS) {
  // ...

  MutexLocker ml(MethodData_lock, THREAD);
  if (method->method_data() == NULL) {
    ClassLoaderData* loader_data = method->method_holder()->class_loader_data();
    MethodData* method_data = MethodData::allocate(loader_data, method, CHECK);
    method->set_method_data(method_data);
  }
}

就是为Method::_method_data属性创建MethodData实例并赋值。所以调用InterpreterRuntime::profile_method()函数会让Method::_method_data属性的值不为NULL。

接着看如下的汇编代码:

// 只有开启UseOnStackReplacement时才会生成如下汇编
// 当超过阈值后会跳转到此分支

// **** backedge_counter_overflow ****

// 对rdx中的数取补码
0x00007fffe101df22: neg    %rdx
// 将r13的地址加到rdx上,这两步是计算跳转地址
0x00007fffe101df25: add    %r13,%rdx

// 回边计数达到阈值后,会
// 通过调用call_VM()函数来调用InterpreterRuntime::frequency_counter_overflow()函数
0x00007fffe101df28: callq  0x00007fffe101df32
0x00007fffe101df2d: jmpq   0x00007fffe101dfb0
0x00007fffe101df32: mov    %rdx,%rsi
0x00007fffe101df35: lea    0x8(%rsp),%rax
0x00007fffe101df3a: mov    %r13,-0x38(%rbp)
0x00007fffe101df3e: mov    %r15,%rdi
0x00007fffe101df41: mov    %rbp,0x200(%r15)
0x00007fffe101df48: mov    %rax,0x1f0(%r15)
0x00007fffe101df4f: test   $0xf,%esp
0x00007fffe101df55: je     0x00007fffe101df6d
0x00007fffe101df5b: sub    $0x8,%rsp
0x00007fffe101df5f: callq  0x00007ffff66b45c8
0x00007fffe101df64: add    $0x8,%rsp
0x00007fffe101df68: jmpq   0x00007fffe101df72
0x00007fffe101df6d: callq  0x00007ffff66b45c8
0x00007fffe101df72: movabs $0x0,%r10
0x00007fffe101df7c: mov    %r10,0x1f0(%r15)
0x00007fffe101df83: movabs $0x0,%r10
0x00007fffe101df8d: mov    %r10,0x200(%r15)
0x00007fffe101df94: cmpq   $0x0,0x8(%r15)
0x00007fffe101df9c: je     0x00007fffe101dfa7
0x00007fffe101dfa2: jmpq   0x00007fffe1000420
0x00007fffe101dfa7: mov    -0x38(%rbp),%r13
0x00007fffe101dfab: mov    -0x30(%rbp),%r14
0x00007fffe101dfaf: retq  
// 结束call_VM()函数的调用


// 恢复待执行的字节码 
0x00007fffe101dfb0: movzbl 0x0(%r13),%ebx
// rax: osr nmethod (osr ok) or NULL (osr not possible)
// ebx: target bytecode
// rdx: scratch
// r14: locals pointer
// r13: bcp

// %rax存放编译的结果,如果为NULL,则表示还没有合适的编译结果,否则需要执行栈上替换操作
// 校验frequency_counter_overflow()函数返回的编译结果是否为空,
// 如果为空则跳转到----dispatch----,即继续解释执行字节码
0x00007fffe101dfb5: test   %rax,%rax
0x00007fffe101dfb8: je     0x00007fffe101de30

// 如果不为空,即表示方法编译完成,将nmethod::_entry_bci属性的偏移复制到rcx中
0x00007fffe101dfbe: mov    0x48(%rax),%ecx
// 如果rcx等于InvalidOSREntryBci,则跳转到----dispatch----
0x00007fffe101dfc1: cmp    $0xfffffffe,%ecx
0x00007fffe101dfc4: je     0x00007fffe101de30

// 开始执行栈上替换
// 注意%rax中已经存储了编译的结果,所以执行这段编译就是执行栈上替换,不过在执行
// 之前,还需要将解释栈转换为编译栈,因为2者的调用约定完全不同

// 将%rax中的值暂时存储在%r13中,因为调用如下的OSR_migration_begin()函数可能
// 会破坏%rax中存储的值
0x00007fffe101dfca: mov    %rax,%r13 

// 通过调用call_VM()函数调用SharedRuntime::OSR_migration_begin()函数
// 调用OSR_migration_begin()函数完成栈帧上变量和monitor的迁移
0x00007fffe101dfcd: callq  0x00007fffe101dfd7
0x00007fffe101dfd2: jmpq   0x00007fffe101e052
0x00007fffe101dfd7: lea    0x8(%rsp),%rax
0x00007fffe101dfdc: mov    %r13,-0x38(%rbp)
0x00007fffe101dfe0: mov    %r15,%rdi
0x00007fffe101dfe3: mov    %rbp,0x200(%r15)
0x00007fffe101dfea: mov    %rax,0x1f0(%r15)
0x00007fffe101dff1: test   $0xf,%esp
0x00007fffe101dff7: je     0x00007fffe101e00f
0x00007fffe101dffd: sub    $0x8,%rsp
0x00007fffe101e001: callq  0x00007ffff6a18a6a
0x00007fffe101e006: add    $0x8,%rsp
0x00007fffe101e00a: jmpq   0x00007fffe101e014
0x00007fffe101e00f: callq  0x00007ffff6a18a6a
0x00007fffe101e014: movabs $0x0,%r10
0x00007fffe101e01e: mov    %r10,0x1f0(%r15)
0x00007fffe101e025: movabs $0x0,%r10
0x00007fffe101e02f: mov    %r10,0x200(%r15)
0x00007fffe101e036: cmpq   $0x0,0x8(%r15)
0x00007fffe101e03e: je     0x00007fffe101e049
0x00007fffe101e044: jmpq   0x00007fffe1000420
0x00007fffe101e049: mov    -0x38(%rbp),%r13
0x00007fffe101e04d: mov    -0x30(%rbp),%r14
0x00007fffe101e051: retq   


// 此时的%rax中存储的是OSR buffer,将其做为第1个参数传递给OSR编译后生成的代码
// 将%rax中的值拷贝到%rsi(j_rarg0)
0x00007fffe101e052: mov    %rax,%rsi

// 获取interpreter_frame_sender_sp_offset偏移处的值
0x00007fffe101e055: mov    -0x8(%rbp),%rdx
// leaveq相当于: movq %rbp,%rsp和pop %rbp
0x00007fffe101e059: leaveq
          
// 将返回地址弹出到%rcx中
0x00007fffe101e05a: pop    %rcx      
0x00007fffe101e05b: mov    %rdx,%rsp 
// -StackAlignmentInBytes的值为$0xfffffffffffffff0,确保栈是按8字节对齐的
0x00007fffe101e05e: and    $0xfffffffffffffff0,%rsp
// 将返回地址压入栈中
0x00007fffe101e062: push   %rcx

// 跳转到nmethod::_osr_entry_point,开始执行
0x00007fffe101e063: jmpq   *0x88(%r13) 

编译热点代码块时,如果调用InterpreterRuntime::frequency_counter_overflow()函数获取到了合适的编译结果,那么就需要执行栈上替换了,替换完成后,解释执行就直接变为编译执行了,关于如何编译热点代码以及如何调用SharedRuntime::OSR_migration_begin()函数完成栈帧迁移等操作在后面会详细介绍,这里暂不介绍。

整个流程如下图所示。

调用InterpreterRuntime::frequency_counter_overflow()函数进行代码块的编译,调用SharedRuntime::OSR_migration_begin()函数进行栈上替换操作,这在后面会详细介绍。

公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流