第54篇-方法調用計數

  • 2022 年 1 月 14 日
  • 筆記

前一篇介紹了編譯策略,編譯策略依賴於性能計數,所以HotSpot VM中有許多性能統計相關的程式碼,如統計方法調用次數的程式碼,統計回邊的次數,這就是方法調用計數器(Invocation Counter )和回邊計數器(Back Edge Counter )。有了這些統計資訊,HotSpot VM就會判斷出是否為熱點程式碼,是否需要觸發即時編譯,這樣的行為稱為熱點探測(Hot Spot Detection)。

HotSpot VM是基於計數器的熱點探測(Counter Based Hot Spot Detection),也就是為每個方法(程式碼塊)建立計數器,執行次數超過閾值就認為是「熱點方法(熱點程式碼塊)」。採用這樣的方式統計的結果精確嚴謹,但是實現起來比較麻煩,也不能直接獲取方法的調用關係。對於方法調用來說,調用次數超過閾值後形成熱點,然後提交編譯任務給後台編譯執行緒,編譯完成後通過適配器對原來位元組碼的調用轉換成對本地程式碼的調用;對於熱點程式碼塊來說,如for循環的次數超過閾值形成熱點,然後會儘可能的立即完成編譯,並且在完成必要的棧幀遷移轉換後立即執行編譯後的本地程式碼,即完成OSR(棧上替換,On-Stack Replacement)。後續我們會介紹如何根據統計到的相關資訊觸發編譯,以及如何對方法和程式碼塊編譯,還有棧幀遷移等內容。

我們先介紹一下Method類中保存統計資訊的一些變數,如下:

class Method : public Metadata {
 private:
  MethodData*       _method_data;
  MethodCounters*   _method_counters;
  // ...
} 

MethodData類中定義了一個_data屬性,這個屬性會記錄函數運行狀態下的數據。主要記錄3部分的數據,一個是函數類型等運行相關統計數據,一個是參數類型運行相關統計數據,還有一個是extra擴展區保存著deoptimization的相關資訊,是C2優化的基礎;MethodCounters主要用於基於調用頻率的熱點方法或塊的跟蹤統計。

MethodData類的定義如下:

class MethodData : public Metadata {
private:
  InvocationCounter _invocation_counter;
  InvocationCounter _backedge_counter;
  // ...
  intptr_t  _data[1];
}

_data能夠記錄非常詳細的函數運行時數據,其中會涉及到ProfileData、Layout等,我們在下一篇將會詳細介紹。

MethodCounters類的實現如下:

class MethodCounters: public MetaspaceObj {
 private:
  // ...
  InvocationCounter _invocation_counter;
  InvocationCounter _backedge_counter;
  // ...
}

MethodCounters用於熱點程式碼跟蹤中的方法調用計數和回邊計數。

將統計相關的資訊存儲到MerthodData和MethodCounters中的相關屬性上,主要是_invocation_counter和_backedge_counter屬性,如下圖所示。

MethodData和MethodCounters這兩個類中都聲明了_invocation_counter與_backedge_counter屬性,屬性的類型為InvocationCounter。InvocationCounter類的定義如下:

class InvocationCounter VALUE_OBJ_CLASS_SPEC {
 private:                             // bit no: |31...3|  2  | 1 0 |
  unsigned int _counter;              // format: [count |carry|state]

 public:
  // 執行方法編譯的閾值
  static int InterpreterInvocationLimit;     
  // 執行棧上替換的閾值
  static int InterpreterBackwardBranchLimit; 
  // 收集解釋器執行性能數據的閾值
  static int InterpreterProfileLimit;       

  typedef address (*Action)(methodHandle method, TRAPS);

 private:
  // 不同State下的閾值
  static int         _init  [number_of_states];  
  // 不同State下的達到閾值執行的動作
  static Action      _action[number_of_states];  
};

每一個方法都會對應一個方法調用計數器,為了儘可能節省記憶體空間,HotSpot VM使用_counter記錄多個數據資訊,這是一個整數類型的組合數字, counter、carry、state 三個欄位用一個32位 int 類型數據表示,其中:

  • counter:第3-31位表示方法調用計數,每次方法調用加1(每增加一次計數,_counter的值要加8,因為低3位不用來計數),超過半衰周期未觸發編譯則數據減半,觸發編譯後調用 reset()函數重置 InvocationCounter 所有數據;
  • carry:第2位表示當前方法是否已被編譯,方法調用計數器達到閾值並觸發編譯動作後,carry 被設置為1,表示該方法已被編譯;
  • state:第0位和第1位表示超出閾值時的處理,枚舉值由InvocationCounter類中的State枚舉類定義,wait_for_nothing 表示方法調用次數超過閾值後不觸發編譯,wait_for_compile 表示方法調用次數超過閾值後觸發編譯。

下面開始介紹解釋執行時的方法調用計數邏輯。我們從InterpreterGenerator::generate_counter_incr()函數看起。InterpreterGenerator::generate_counter_incr()函數會生成常式,常式的實現邏輯非常簡單,檢查Method::_method_data和Mehod::_method_counters屬性是否為NULL,如果不為NULL,則分別向Method::_method_data::_invocation_counter::_counter和Method::_method_counters::_invocation_counter::_counter屬性中增加調用計數即可。同時還會檢查計數值是否溢出,如果要溢出,還會觸發編譯方法的邏輯。

InterpreterGenerator::generate_counter_incr()函數會被如下函數調用:

(1)InterpreterGenerator::generate_normal_entry()函數

(2)InterpreterGenerator::generate_native_entry()函數

在開啟了UseCompiler或CountCompiledCalls選項的情況下才會進行調用次數的統計。

1、InterpreterGenerator::generate_counter_incr()函數

在調用常式之前,rbx中存儲著Method實例的地址。

InterpreterGenerator::generate_counter_incr()函數在開啟分層編譯的情況下(TieredCompilation)生成的彙編程式碼如下:

// 在開啟分層編譯TieredCompilation和性能收集ProfileInterpreter情況下
// 生成如下彙編程式碼

// 校驗Method::_method_data屬性非空,如果為空則跳轉到----no_mdo----
0x00007fffe1010490: mov    0x18(%rbx),%rax
0x00007fffe1010494: test   %rax,%rax
0x00007fffe1010497: je     0x00007fffe10104b3

// 獲取Method::_method_data::_invocation_counter::_counter並存儲到%ecx
0x00007fffe1010499: mov    0x6c(%rax),%ecx
// 增加一次調用,一次調用需要增加8,因為_counter的後3位不表示計數
0x00007fffe101049c: add    $0x8,%ecx
// 將增加調用次數後的值存儲回Method::_method_data::_invocation_counter::_counter中
0x00007fffe101049f: mov    %ecx,0x6c(%rax)
// 將更新後的Method::_method_data::_invocation_counter::_counter與1111111 000 進行與運算
0x00007fffe10104a2: and    $0x3f8,%ecx
// 如果為0,就表示溢出,則跳轉到----overflow----
0x00007fffe10104a8: je     0x00007fffe10104ae
// 如果計數沒有溢出,則跳轉到----done----
0x00007fffe10104ae: jmpq   0x00007fffe101056a 

下面接著看InterpreterGenerator::generate_counter_incr()函數生成的彙編,如下:

// **** no_mdo ****

// 調用InterpreterMacroAssembler::get_method_counters()函數
// 獲取Method::_method_counters屬
// 性的值後測試,如果不為0,則跳轉到----has_counters----
0x00007fffe10104b3: mov    0x20(%rbx),%rax
0x00007fffe10104b7: test   %rax,%rax
0x00007fffe10104ba: jne    0x00007fffe1010555

// 通過call_VM()函數生成的如下常式調用InterpreterRuntime::build_method_counters()函數
0x00007fffe10104c0: callq  0x00007fffe10104ca
0x00007fffe10104c5: jmpq   0x00007fffe1010548
0x00007fffe10104ca: mov    %rbx,%rsi
0x00007fffe10104cd: lea    0x8(%rsp),%rax
0x00007fffe10104d2: mov    %r13,-0x38(%rbp)
0x00007fffe10104d6: mov    %r15,%rdi
0x00007fffe10104d9: mov    %rbp,0x200(%r15)
0x00007fffe10104e0: mov    %rax,0x1f0(%r15)
0x00007fffe10104e7: test   $0xf,%esp
0x00007fffe10104ed: je     0x00007fffe1010505
0x00007fffe10104f3: sub    $0x8,%rsp
0x00007fffe10104f7: callq  0x00007ffff66b587e
0x00007fffe10104fc: add    $0x8,%rsp
0x00007fffe1010500: jmpq   0x00007fffe101050a
0x00007fffe1010505: callq  0x00007ffff66b587e
0x00007fffe101050a: movabs $0x0,%r10
0x00007fffe1010514: mov    %r10,0x1f0(%r15)
0x00007fffe101051b: movabs $0x0,%r10
0x00007fffe1010525: mov    %r10,0x200(%r15)
0x00007fffe101052c: cmpq   $0x0,0x8(%r15)
0x00007fffe1010534: je     0x00007fffe101053f
0x00007fffe101053a: jmpq   0x00007fffe1000420
0x00007fffe101053f: mov    -0x38(%rbp),%r13
0x00007fffe1010543: mov    -0x30(%rbp),%r14
0x00007fffe1010547: retq   
// 結束了調用call_VM()函數生成的常式序


// 獲取Method::_method_counters屬性的值
// 後測試,如果仍然為0,則跳轉到----done---- 
0x00007fffe1010548: mov 0x20(%rbx),%rax 
0x00007fffe101054c: test %rax,%rax 
0x00007fffe101054f: je 0x00007fffe101056a

// **** has_counters ****

// 更新Method::_method_counters::_invocation_counter::_counter屬
// 性的值,如果此屬性的值為0,則跳轉到----overflow----
0x00007fffe1010555: mov    0x8(%rax),%ecx
0x00007fffe1010558: add    $0x8,%ecx
0x00007fffe101055b: mov    %ecx,0x8(%rax)
0x00007fffe101055e: and    $0x3f8,%ecx
0x00007fffe1010564: je     0x00007fffe101056a


// **** done ****

// **** overflow ****

如上彙編實現的邏輯非常簡單,檢查Method::_method_data和Mehod::_method_counters屬性是否為NULL,如果不為NULL,則分別向Method::_method_data::_invocation_counter::_counter和Method::_method_counters::_invocation_counter::_counter屬性中增加調用計數即可。

當Mehod::_method_counters的屬性值為NULL時,還可以調用InterpreterRuntime::build_method_counters()函數嘗試一下賦值操作。函數的實現如下:

IRT_ENTRY(MethodCounters*, InterpreterRuntime::build_method_counters(JavaThread* thread, Method* m))
  MethodCounters* mcs = Method::build_method_counters(m, thread);
  return mcs;
IRT_END

MethodCounters* Method::build_method_counters(Method* m, TRAPS) {
  methodHandle mh(m);
  ClassLoaderData* loader_data = mh->method_holder()->class_loader_data();
  MethodCounters* counters = MethodCounters::allocate(loader_data, CHECK_NULL);
  if (mh->method_counters() == NULL) {
    mh->set_method_counters(counters);
  } else {
    MetadataFactory::free_metadata(loader_data, counters);
  }
  return mh->method_counters();
}

可能會創建一個MethodCounters實例並賦值給Mehod::_method_counters。

2、interpreterGenerator::generate_normal_entry()函數統計資訊

此函數中實現的與統計相關的邏輯如下:

Label  invocation_counter_overflow;
if (inc_counter) {
    generate_counter_incr(&invocation_counter_overflow,_,_); 
}

調用generate_counter_incr()函數時傳遞的參數以_來表示的代表這個參數不需要關注。當開啟了UseCompiler或CountCompiledCalls選項時,inc_counter的值為true。

後面對調用計數器溢出的處理邏輯如下:

if (inc_counter) {
    // ...
    // 當計數器溢出或某個方法需要編譯時跳轉到此處
    __ bind(invocation_counter_overflow);
    // 觸發編譯任務
    generate_counter_overflow(&continue_after_compile);
}

這一段程式碼其實在dispatch_next()函數生成的常式之後,在分派位元組碼的常式中有跳轉邏輯,所以如上程式碼只能是那些跳轉到invocation_counter_overflow處的常式才會有執行的機會,執行完成後又跳回了continue_after_compile處繼續解釋執行,continue_after_compile處的常式在dispatch_next()函數生成的常式之前。

如上片段生成的彙編程式碼如下:

// ...省略不支援分層編譯下的一些統計相關彙編程式碼

// **** invocation_counter_overflow ****
// 當計數器溢出或某個方法需要編譯時跳轉到此處
// 在執行如下的彙編程式碼時,各個暫存器中的值如下:
// r14 - locals
// r13 - bcp
// rbx - method
// edx - cpool --- DOES NOT APPEAR TO BE TRUE
// rbp - interpreter frame


// 將c_rarg1暫存器的值設置為0
0x00007fffe1010b34: mov    $0x0,%esi
// 調用call_VM()函數生成常式來調用InterpreterRuntime::frequency_counter_overflow()函數
0x00007fffe1010b39: callq  0x00007fffe1010b43
0x00007fffe1010b3e: jmpq   0x00007fffe1010bbe
0x00007fffe1010b43: lea    0x8(%rsp),%rax
0x00007fffe1010b48: mov    %r13,-0x38(%rbp)
0x00007fffe1010b4c: mov    %r15,%rdi
0x00007fffe1010b4f: mov    %rbp,0x200(%r15)
0x00007fffe1010b56: mov    %rax,0x1f0(%r15)
0x00007fffe1010b5d: test   $0xf,%esp
0x00007fffe1010b63: je     0x00007fffe1010b7b
0x00007fffe1010b69: sub    $0x8,%rsp
0x00007fffe1010b6d: callq  0x00007ffff66b4b98
0x00007fffe1010b72: add    $0x8,%rsp
0x00007fffe1010b76: jmpq   0x00007fffe1010b80
0x00007fffe1010b7b: callq  0x00007ffff66b4b98
0x00007fffe1010b80: movabs $0x0,%r10
0x00007fffe1010b8a: mov    %r10,0x1f0(%r15)
0x00007fffe1010b91: movabs $0x0,%r10
0x00007fffe1010b9b: mov    %r10,0x200(%r15)
0x00007fffe1010ba2: cmpq   $0x0,0x8(%r15)
0x00007fffe1010baa: je     0x00007fffe1010bb5
0x00007fffe1010bb0: jmpq   0x00007fffe1000420
0x00007fffe1010bb5: mov    -0x38(%rbp),%r13
0x00007fffe1010bb9: mov    -0x30(%rbp),%r14
0x00007fffe1010bbd: retq   

// 恢復rbx中的Method*
0x00007fffe1010bbe: mov    -0x18(%rbp),%rbx
// 跳轉到----do_continue----
0x00007fffe1010bc2: jmpq   0x00007fffe101096a 
   

如上只給出了支援分層編譯並且ProfilerInterpreter選項為true情況下的統計邏輯,省略了不支援分層編譯下的統計邏輯。

對於InterpreterGenerator::generate_native_entry()函數來說,其生成的邏輯和terpreterGenerator::generate_normal_entry()函數基本一致,沒有什麼特別的處理,後面我們會繼續介紹如何編譯Java方法和本地方法。

整個過程的一個執行邏輯如下:

從圖中我們能夠清析的看到,在解釋執行的情況下,如果Method::_method_data屬性的值不為NULL時,會優先增加此屬性的調用計數相關的屬性值(因為MethodData能更精確的記錄更多運行時數據),只有當Method::_method_data為空時,才會參考Method::_method_counters屬性的值。一般在創建Method實例時,2個屬性的值都為NULL,在分層編譯的情況下,一般會調用InterpreterRuntime::build_method_counters()函數創建的MethodCounters實例進行計數。 

公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源程式碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流
 

 

 

  

 

Tags: