第10篇-初始化模板表
- 2021 年 8 月 25 日
- 筆記
在 第9篇-位元組碼指令的定義 我們介紹了位元組碼指令並且將位元組碼指令相關的資訊都存儲到了相關數組中,只需要通過Opcode就可從相關數組中獲取對應的資訊。
在init_globals()函數中調用bytecodes_init()函數初始化好位元組碼指令後會調用interpreter_init()函數初始化解釋器。函數最終會調用到TemplateInterpreter::initialize()函數。這個函數的實現如下:
源程式碼位置:/src/share/vm/interpreter/templateInterpreter.cpp void TemplateInterpreter::initialize() { if (_code != NULL) return; // 抽象解釋器AbstractInterpreter的初始化, // AbstractInterpreter是基於彙編模型的解釋器的共同基類, // 定義了解釋器和解釋器生成器的抽象介面 AbstractInterpreter::initialize(); // 模板表TemplateTable的初始化,模板表TemplateTable保存了各個位元組碼的模板 TemplateTable::initialize(); // generate interpreter { ResourceMark rm; int code_size = InterpreterCodeSize; // CodeCache的Stub隊列StubQueue的初始化 _code = new StubQueue(new InterpreterCodeletInterface, code_size, NULL,"Interpreter"); // 實例化模板解釋器生成器對象TemplateInterpreterGenerator InterpreterGenerator g(_code); } // 初始化位元組分發表 _active_table = _normal_table; }
這個初始化函數中涉及到的初始化邏輯比較多,而且比較複雜。我們將初始化分為4部分:
(1)抽象解釋器AbstractInterpreter的初始化,AbstractInterpreter是基於彙編模型的解釋器的共同基類,定義了解釋器和解釋器生成器的抽象介面。
(2)模板表TemplateTable的初始化,模板表TemplateTable保存了各個位元組碼的模板(目標程式碼生成函數和參數);
(3)CodeCache的Stub隊列StubQueue的初始化;
(4)解釋器生成器InterpreterGenerator的初始化。
其中抽象解釋器初始化時會涉及到一些計數,這些計數主要與編譯執行有關,所以這裡暫不過多介紹,到後面介紹編譯執行時再介紹。
下面我們分別介紹如上3個部分的初始化過程,這一篇只介紹模板表的初始化過程。
函數TemplateTable::initialize()的實現如下:
模板表TemplateTable保存了各個位元組碼的執行模板(目標程式碼生成函數和參數),而前一篇介紹對位元組碼的定義已經進行了詳細介紹,執行模板定義的是每個位元組碼如何在解釋模式下執行的。initialize()函數的實現如下:
源程式碼位置:/src/share/vm/interpreter/templateInterpreter.cpp void TemplateTable::initialize() { if (_is_initialized) return; _bs = Universe::heap()->barrier_set(); // For better readability const char _ = ' '; const int ____ = 0; const int ubcp = 1 << Template::uses_bcp_bit; const int disp = 1 << Template::does_dispatch_bit; const int clvm = 1 << Template::calls_vm_bit; const int iswd = 1 << Template::wide_bit; // interpr. templates // Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _ ); def(Bytecodes::_aconst_null , ____|____|____|____, vtos, atos, aconst_null , _ ); def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 ); def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst , 0 ); // ... def(Bytecodes::_tableswitch , ubcp|disp|____|____, itos, vtos, tableswitch , _ ); def(Bytecodes::_lookupswitch , ubcp|disp|____|____, itos, itos, lookupswitch , _ ); def(Bytecodes::_ireturn , ____|disp|clvm|____, itos, itos, _return , itos ); def(Bytecodes::_lreturn , ____|disp|clvm|____, ltos, ltos, _return , ltos ); def(Bytecodes::_freturn , ____|disp|clvm|____, ftos, ftos, _return , ftos ); def(Bytecodes::_dreturn , ____|disp|clvm|____, dtos, dtos, _return , dtos ); def(Bytecodes::_areturn , ____|disp|clvm|____, atos, atos, _return , atos ); def(Bytecodes::_return , ____|disp|clvm|____, vtos, vtos, _return , vtos ); def(Bytecodes::_getstatic , ubcp|____|clvm|____, vtos, vtos, getstatic , f1_byte ); def(Bytecodes::_putstatic , ubcp|____|clvm|____, vtos, vtos, putstatic , f2_byte ); def(Bytecodes::_getfield , ubcp|____|clvm|____, vtos, vtos, getfield , f1_byte ); def(Bytecodes::_putfield , ubcp|____|clvm|____, vtos, vtos, putfield , f2_byte ); def(Bytecodes::_invokevirtual , ubcp|disp|clvm|____, vtos, vtos, invokevirtual , f2_byte ); def(Bytecodes::_invokespecial , ubcp|disp|clvm|____, vtos, vtos, invokespecial , f1_byte ); def(Bytecodes::_invokestatic , ubcp|disp|clvm|____, vtos, vtos, invokestatic , f1_byte ); def(Bytecodes::_invokeinterface , ubcp|disp|clvm|____, vtos, vtos, invokeinterface , f1_byte ); def(Bytecodes::_invokedynamic , ubcp|disp|clvm|____, vtos, vtos, invokedynamic , f1_byte ); def(Bytecodes::_new , ubcp|____|clvm|____, vtos, atos, _new , _ ); def(Bytecodes::_newarray , ubcp|____|clvm|____, itos, atos, newarray , _ ); def(Bytecodes::_anewarray , ubcp|____|clvm|____, itos, atos, anewarray , _ ); def(Bytecodes::_arraylength , ____|____|____|____, atos, itos, arraylength , _ ); def(Bytecodes::_athrow , ____|disp|____|____, atos, vtos, athrow , _ ); def(Bytecodes::_checkcast , ubcp|____|clvm|____, atos, atos, checkcast , _ ); def(Bytecodes::_instanceof , ubcp|____|clvm|____, atos, itos, instanceof , _ ); def(Bytecodes::_monitorenter , ____|disp|clvm|____, atos, vtos, monitorenter , _ ); def(Bytecodes::_monitorexit , ____|____|clvm|____, atos, vtos, monitorexit , _ ); def(Bytecodes::_wide , ubcp|disp|____|____, vtos, vtos, wide , _ ); def(Bytecodes::_multianewarray , ubcp|____|clvm|____, vtos, atos, multianewarray , _ ); def(Bytecodes::_ifnull , ubcp|____|clvm|____, atos, vtos, if_nullcmp , equal ); def(Bytecodes::_ifnonnull , ubcp|____|clvm|____, atos, vtos, if_nullcmp , not_equal ); def(Bytecodes::_goto_w , ubcp|____|clvm|____, vtos, vtos, goto_w , _ ); def(Bytecodes::_jsr_w , ubcp|____|____|____, vtos, vtos, jsr_w , _ ); // wide Java spec bytecodes def(Bytecodes::_iload , ubcp|____|____|iswd, vtos, itos, wide_iload , _ ); def(Bytecodes::_lload , ubcp|____|____|iswd, vtos, ltos, wide_lload , _ ); // ... // JVM bytecodes // ... def(Bytecodes::_shouldnotreachhere , ____|____|____|____, vtos, vtos, shouldnotreachhere , _ ); }
TemplateTable的初始化調用def()將所有位元組碼的目標程式碼生成函數和參數保存在_template_table或_template_table_wide(wide指令)模板數組中。除了虛擬機規範本身定義的位元組碼指令外,HotSpot虛擬機也定義了一些位元組碼指令,這些指令為了輔助虛擬機進行更好的功能實現,例如Bytecodes::_return_register_finalizer等在之前已經介紹過,可以更好的實現finalizer類型對象的註冊功能。
我們只給出部分位元組碼指令的模板定義,調用def()函數對每個位元組碼指令的模板進行定義,傳遞的參數是我們關注的重點:
(1)指出為哪個位元組碼指令定義模板
(2)ubcp|disp|clvm|iswd,這是一個組合數字,具體的數字與Template中定義的枚舉類緊密相關,枚舉類中定義的常量如下:
enum Flags { uses_bcp_bit, // set if template needs the bcp pointing to bytecode does_dispatch_bit, // set if template dispatches on its own 就其本身而言; 靠自己 calls_vm_bit, // set if template calls the vm wide_bit // set if template belongs to a wide instruction };
下面詳細解釋這幾個參數,如下:
- uses_bcp_bit,標誌需要使用位元組碼指針(byte code pointer,數值為位元組碼基址+位元組碼偏移量)。表示生成的模板程式碼中是否需要使用指向位元組碼指令的指針,其實也就是說是否需要讀取位元組碼指令的操作數,所以含有操作數的指令大部分都需要bcp,但是有一些是不需要的,如monitorenter與monitorexit等,這些的操作數都在表達式棧中,表達式棧頂就是其操作數,並不需要從Class文件中讀取,所以不需要bcp;
- does_dispatch_bit,標誌表示自己是否含有控制流轉發邏輯,如tableswitch、lookupswitch、invokevirtual、ireturn等位元組碼指令,本身就需要進行控制流轉發;
- calls_vm_bit,標誌是否需要調用JVM函數,在調用TemplateTable::call_VM()函數時都會判斷是否有這個標誌,通常方法調用JVM函數時都會通過調用TemplateTable::call_VM()函數來間接完成調用。JVM函數就是用C++寫的函數。
- wide_bit,標誌是否是wide指令(使用附加位元組擴展全局變數索引)
(3)_tos_in與_tos_out:表示模板執行前與模板執行後的TosState(操作數棧棧頂元素的數據類型,TopOfStack,用來檢查模板所聲明的輸出輸入類型是否和該函數一致,以確保棧頂元素被正確使用)。
_tos_in與_tos_out的值必須是枚舉類中定義的常量,如下:
enum TosState { // describes the tos cache contents btos = 0, // byte, bool tos cached ctos = 1, // char tos cached stos = 2, // short tos cached itos = 3, // int tos cached ltos = 4, // long tos cached ftos = 5, // float tos cached dtos = 6, // double tos cached atos = 7, // object cached vtos = 8, // tos not cached number_of_states, ilgl // illegal state: should not occur };
如iload指令,執行之前棧頂狀態為vtos,表示並不會使用棧頂的數據,所以如果程式為了提高執行效率將上一次執行的結果快取到了暫存器中,那麼此時就應該在執行iload指令之前將這個暫存器的值壓入棧頂。iload指令執行之後的棧頂狀態為itos,因為iload是向操作數棧中壓入一個整數,所以此時的棧頂狀態為int類型,那麼這個值可以快取到暫存器中,假設下一個指令為ireturn,那麼棧頂之前與之後的狀態分別為itos和itos,那麼可直接將快取在暫存器中的int類型返回即可,不需要做任何和操作數棧相關的操作。
(4)_gen與_arg:_gen表示模板生成器(函數指針),這個函數會為對應的位元組碼生成對應的執行邏輯;_arg表示為模板生成器傳遞的參數。調用函數指針會為每個位元組碼指令按其語義針對不同的平台上生成不同的機器指令,這裡我們只討論x86架構下64位的機器指令實現,由於機器指令很難讀懂,所以我們後續只閱讀由機器指令反編譯的彙編指令。
下面看一下TemplateTable::initialize()函數中調用的Template::def()函數,如下:
void TemplateTable::def( Bytecodes::Code code, // 位元組碼指令 int flags, // 標誌位 TosState in, // 模板執行前TosState TosState out, // 模板執行後TosState void (*gen)(int arg), // 模板生成器,是模板的核心組件 int arg ) { // 表示是否需要bcp指針 const int ubcp = 1 << Template::uses_bcp_bit; // 表示是否在模板範圍內進行轉發 const int disp = 1 << Template::does_dispatch_bit; // 表示是否需要調用JVM函數 const int clvm = 1 << Template::calls_vm_bit; // 表示是否為wide指令 const int iswd = 1 << Template::wide_bit; // 如果是允許在位元組碼指令前加wide位元組碼指令的一些指令,那麼 // 會使用_template_table_wild模板數組進行位元組碼轉發,否則 // 使用_template_table模板數組進行轉發 bool is_wide = (flags & iswd) != 0; Template* t = is_wide ? template_for_wide(code) : template_for(code); // 調用模板表t的initialize()方法初始化模板表 t->initialize(flags, in, out, gen, arg); }
模板表由模板表數組與一組生成器組成:
模板數組有_template_table與_template_table_wild,數組的下標為位元組碼的Opcode,值為Template。定義如下:
Template TemplateTable::_template_table[Bytecodes::number_of_codes]; Template TemplateTable::_template_table_wide[Bytecodes::number_of_codes];
模板數組的值為Template,這個Template類中定義了保存標誌位flags的_flags屬性,保存棧頂快取狀態in和out的_tos_in和_tos_out,還有保存生成器gen及參數arg的_gen與_arg,所以調用t->initialize()後其實是初始化Template中的變數。initialize()函數的實現如下:
void Template::initialize( int flags, TosState tos_in, TosState tos_out, generator gen, int arg ) { _flags = flags; _tos_in = tos_in; _tos_out = tos_out; _gen = gen; _arg = arg; }
不過這裡並不會調用gen函數生成對應的彙編程式碼,只是將傳遞給def()函數的各種資訊保存到Template實例中,在TemplateTable::def()函數中,通過template_for()或template_for_wild()函數獲取到數組中對應的Template實例後,就會調用Template::initialize()函數將資訊保存到對應的Template實例中,這樣就可以根據位元組碼索引從數組中獲取對應的Template實例,進而獲取位元組碼指令模板的相關資訊。
雖然這裡並不會調用gen來生成位元組碼指令對應的機器指令,但是我們可以提前看一下gen這個指針函數是怎麼為某個位元組碼指令生成對應的機器指令的。
看一下TemplateTable::initialize()函數中對def()函數的調用,以_iinc(將局部變數表中對應的slot位的值增加1)為例,調用如下:
def( Bytecodes::_iinc, // 位元組碼指令 ubcp|____|clvm|____, // 標誌 vtos, // 模板執行前的TosState vtos, // 模板執行後的TosState iinc , // 模板生成器,是一個iinc()函數的指針 _ // 不需要模板生成器參數 );
設置標誌位uses_bcp_bit和calls_vm_bit,表示iinc指令的生成器需要使用bcp指針函數at_bcp(),且需要調用JVM函數,下面給出了生成器的定義:
源程式碼位置:/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp void TemplateTable::iinc() { transition(vtos, vtos); __ load_signed_byte(rdx, at_bcp(2)); // get constant locals_index(rbx); __ addl(iaddress(rbx), rdx); }
由於iinc指令只涉及到對局部變數表的操作,並不會影響操作數棧,也不需要使用操作數棧頂的值,所以棧頂之前與之後的狀態為vtos與vtos,調用transition()函數只是驗證棧頂快取的狀態是否正確。
iinc指令的位元組碼格式如下:
iinc index // 局部變數表索引值 const // 將局部變數表索引值對應的slot值加const
操作碼iinc佔用一個位元組,而index與const分別佔用一個位元組。使用at_bcp()函數獲取iinc指令的操作數,2表示偏移2位元組,所以會將const取出來存儲到rdx中。調用locals_index()函數取出index,locals_index()就是JVM函數。最終生成的彙編如下:
// %r13存儲的是指向位元組碼的指針,偏移 // 2位元組後取出const存儲到%edx movsbl 0x2(%r13),%edx // 取出index存儲到%ebx movzbl 0x1(%r13),%ebx neg %rbx // %r14指向本地變數表的首地址,將%edx加到 // %r14+%rbx*8指向的記憶體所存儲的值上 // 之所以要對%rbx執行neg進行符號反轉, // 是因為在Linux內核的作業系統上, // 棧是向低地址方向生長的 add %edx,(%r14,%rbx,8)
注釋解釋的已經非常清楚了,這裡不再過多介紹。
推薦閱讀:
第2篇-JVM虛擬機這樣來調用Java主類的main()方法
如果有問題可直接評論留言或加作者微信mazhimazh
關注公眾號,有HotSpot VM源碼剖析系列文章!