第55篇-回邊計數

  • 2022 年 1 月 17 日
  • 筆記

在前面介紹控制轉移指令時只簡單介紹了相關位元組碼解釋執行的主要邏輯,沒有介紹過統計相關的邏輯。對於控制轉移指令來說,通常會調用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,拉你入虛擬機群交流