第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,拉你入虛擬機器群交流