invokevirtual字節碼指令的模板定義如下:
def(Bytecodes::_invokevirtual , ubcp|disp|clvm|____, vtos, vtos, invokevirtual , f2_byte );
生成函數為invokevirtual,傳遞的參數為f2_byte,也就是2,如果為2時,ConstantPoolCacheEntry::indices中取[b2,b1,index]的b2。調用的TemplateTable::invokevirtual()函數的實現如下:
void TemplateTable::invokevirtual(int byte_no) { prepare_invoke(byte_no, rbx, // method or vtable index noreg, // unused itable index rcx, // recv rdx); // flags // rbx: index // rcx: receiver // rdx: flags invokevirtual_helper(rbx, rcx, rdx); }
先調用prepare_invoke()函數,后調用invokevirtual_helper()函數來生成invokevirtual字節碼指令對應的匯編代碼。
1、TemplateTable::prepare_invoke()函數
調用TemplateTable::prepare_invoke()函數生成的匯編代碼比較多,所以我們分三部分進行查看。
第1部分:
0x00007fffe1021f90: mov %r13,-0x38(%rbp) // 將bcp保存到棧中 // invokevirtual x中取出x,也就是常量池索引存儲到%edx, // 其實這里已經是ConstantPoolCacheEntry的index,因為在類的連接 // 階段會對方法中特定的一些字節碼指令進行重寫 0x00007fffe1021f94: movzwl 0x1(%r13),%edx // 將ConstantPoolCache的首地址存儲到%rcx 0x00007fffe1021f99: mov -0x28(%rbp),%rcx // 左移2位,因為%edx中存儲的是ConstantPoolCacheEntry索引,左移2位是因為 // ConstantPoolCacheEntry占用4個字 0x00007fffe1021f9d: shl $0x2,%edx // 計算%rcx+%rdx*8+0x10,獲取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices // 因為ConstantPoolCache的大小為0x16字節,%rcx+0x10定位 // 到第一個ConstantPoolCacheEntry的位置 // %rdx*8算出來的是相對於第一個ConstantPoolCacheEntry的字節偏移 0x00007fffe1021fa0: mov 0x10(%rcx,%rdx,8),%ebx // 獲取ConstantPoolCacheEntry中indices[b2,b1,constant pool index]中的b2 0x00007fffe1021fa4: shr $0x18,%ebx // 取出indices中含有的b2,即bytecode存儲到%ebx中 0x00007fffe1021fa7: and $0xff,%ebx // 查看182的bytecode是否已經連接 0x00007fffe1021fad: cmp $0xb6,%ebx // 如果連接就進行跳轉,跳轉到resolved 0x00007fffe1021fb3: je 0x00007fffe1022052
主要查看字節碼是否已經連接,如果沒有連接則需要連接,如果已經進行了連接,則跳轉到resolved直接執行方法調用操作。
第2部分:
// 調用InterpreterRuntime::resolve_invoke()函數,因為指令還沒有連接 // 將bytecode為182的指令移動到%ebx中 0x00007fffe1021fb9: mov $0xb6,%ebx // 通過調用MacroAssembler::call_VM()函數來調用 // InterpreterRuntime::resolve_invoke(JavaThread* thread, Bytecodes::Code bytecode)函數 // 進行方法連接 0x00007fffe1021fbe: callq 0x00007fffe1021fc8 0x00007fffe1021fc3: jmpq 0x00007fffe1022046 // 跳轉到----E---- // 准備第2個參數,也就是bytecode 0x00007fffe1021fc8: mov %rbx,%rsi 0x00007fffe1021fcb: lea 0x8(%rsp),%rax 0x00007fffe1021fd0: mov %r13,-0x38(%rbp) 0x00007fffe1021fd4: mov %r15,%rdi 0x00007fffe1021fd7: mov %rbp,0x200(%r15) 0x00007fffe1021fde: mov %rax,0x1f0(%r15) 0x00007fffe1021fe5: test $0xf,%esp 0x00007fffe1021feb: je 0x00007fffe1022003 0x00007fffe1021ff1: sub $0x8,%rsp 0x00007fffe1021ff5: callq 0x00007ffff66ac528 0x00007fffe1021ffa: add $0x8,%rsp 0x00007fffe1021ffe: jmpq 0x00007fffe1022008 0x00007fffe1022003: callq 0x00007ffff66ac528 0x00007fffe1022008: movabs $0x0,%r10 0x00007fffe1022012: mov %r10,0x1f0(%r15) 0x00007fffe1022019: movabs $0x0,%r10 0x00007fffe1022023: mov %r10,0x200(%r15) 0x00007fffe102202a: cmpq $0x0,0x8(%r15) 0x00007fffe1022032: je 0x00007fffe102203d 0x00007fffe1022038: jmpq 0x00007fffe1000420 0x00007fffe102203d: mov -0x38(%rbp),%r13 0x00007fffe1022041: mov -0x30(%rbp),%r14 0x00007fffe1022045: retq // 結束MacroAssembler::call_VM()函數的調用 // **** E **** // 將invokevirtual x中的x加載到%edx中,也就是ConstantPoolCacheEntry的索引 0x00007fffe1022046: movzwl 0x1(%r13),%edx // 將ConstantPoolCache的首地址存儲到%rcx中 0x00007fffe102204b: mov -0x28(%rbp),%rcx // %edx中存儲的是ConstantPoolCacheEntry index,轉換為字偏移 0x00007fffe102204f: shl $0x2,%edx
方法連接的邏輯和之前介紹的字段的連接邏輯類似,都是完善ConstantPoolCache中對應的ConstantPoolCacheEntry添加相關信息。
調用InterpreterRuntime::resolve_invoke()函數進行方法連接,這個函數的實現比較多,我們在下一篇中詳細介紹。連接完成后ConstantPoolCacheEntry中的各個項如下圖所示。
所以對於invokevirtual來說,通過vtable進行方法的分發,在ConstantPoolCacheEntry中,_f1字段沒有使用,而對_f2字段來說,如果調用的是非final的virtual方法,則保存的是目標方法在vtable中的索引編號,如果是virtual final方法,則_f2字段直接指向目標方法的Method實例。
第3部分:
// **** resolved **** // resolved的定義點,到這里說明invokevirtual字節碼已經連接 // 獲取ConstantPoolCacheEntry::_f2,這個字段只對virtual有意義 // 在計算時,因為ConstantPoolCacheEntry在ConstantPoolCache之后保存, // 所以ConstantPoolCache為0x10,而 // _f2還要偏移0x10,這樣總偏移就是0x20 // ConstantPoolCacheEntry::_f2存儲到%rbx 0x00007fffe1022052: mov 0x20(%rcx,%rdx,8),%rbx // ConstantPoolCacheEntry::_flags存儲到%edx 0x00007fffe1022057: mov 0x28(%rcx,%rdx,8),%edx // 將flags移動到ecx中 0x00007fffe102205b: mov %edx,%ecx // 從flags中取出參數大小 0x00007fffe102205d: and $0xff,%ecx // 獲取到recv,%rcx中保存的是參數大小,最終計算參數所需要的大小為%rsp+%rcx*8-0x8, // flags中的參數大小對實例方法來說,已經包括了recv的大小 // 如調用實例方法的第一個參數是this(recv) 0x00007fffe1022063: mov -0x8(%rsp,%rcx,8),%rcx // recv保存到%rcx // 將flags存儲到r13中 0x00007fffe1022068: mov %edx,%r13d // 從flags中獲取return type,也就是從_flags的高4位保存的TosState 0x00007fffe102206b: shr $0x1c,%edx // 將TemplateInterpreter::invoke_return_entry地址存儲到%r10 0x00007fffe102206e: movabs $0x7ffff73b6380,%r10 // %rdx保存的是return type,計算返回地址 // 因為TemplateInterpreter::invoke_return_entry是數組, // 所以要找到對應return type的入口地址 0x00007fffe1022078: mov (%r10,%rdx,8),%rdx // 向棧中壓入返回地址 0x00007fffe102207c: push %rdx // 還原ConstantPoolCacheEntry::_flags 0x00007fffe102207d: mov %r13d,%edx // 還原bcp 0x00007fffe1022080: mov -0x38(%rbp),%r13
TemplateInterpreter::invoke_return_entry保存了一段例程的入口,這段例程在后面會詳細介紹。
執行完如上的代碼后,已經向相關的寄存器中存儲了相關的值。相關的寄存器狀態如下:
rbx: 存儲的是ConstantPoolCacheEntry::_f2屬性的值 rcx: 就是調用實例方法時的第一個參數this rdx: 存儲的是ConstantPoolCacheEntry::_flags屬性的值
棧的狀態如下圖所示。
棧中壓入了TemplateInterpreter::invoke_return_entry的返回地址。
2、TemplateTable::invokevirtual_helper()函數
調用TemplateTable::invokevirtual_helper()函數生成的代碼如下:
// flags存儲到%eax 0x00007fffe1022084: mov %edx,%eax // 測試調用的方法是否為final 0x00007fffe1022086: and $0x100000,%eax // 如果不為final就直接跳轉到----notFinal---- 0x00007fffe102208c: je 0x00007fffe10220c0 // 通過(%rcx)來獲取receiver的值,如果%rcx為空,則會引起OS異常 0x00007fffe1022092: cmp (%rcx),%rax // 省略統計相關代碼部分 // 設置調用者棧頂並保存 0x00007fffe10220b4: lea 0x8(%rsp),%r13 0x00007fffe10220b9: mov %r13,-0x10(%rbp) // 跳轉到Method::_from_interpretered_entry入口去執行 0x00007fffe10220bd: jmpq *0x58(%rbx)
對於final方法來說,其實沒有動態分派,所以也不需要通過vtable進行目標查找。調用時的棧如下圖所示。
如下代碼是通過vtable查找動態分派需要調用的方法入口 。
// **** notFinal **** // invokevirtual指令調用的如果是非final方法,直接跳轉到這里 // %rcx中存儲的是receiver,用oop來表示。通過oop獲取Klass 0x00007fffe10220c0: mov 0x8(%rcx),%eax // 調用MacroAssembler::decode_klass__not_null()函數生成下面的一個匯編代碼 0x00007fffe10220c3: shl $0x3,%rax // LogKlassAlignmentInBytes=0x03 // 省略統計相關代碼部分 // %rax中存儲的是recv_klass // %rbx中存儲的是vtable_index, // 而0x1b8為InstanceKlass::vtable_start_offset()*wordSize+vtableEntry::method_offset_in_bytes(), // 其實就是通過動態分派找到需要調用的Method*並存儲到%rbx中 0x00007fffe1022169: mov 0x1b8(%rax,%rbx,8),%rbx // 設置調用者的棧頂地址並保存 0x00007fffe1022171: lea 0x8(%rsp),%r13 0x00007fffe1022176: mov %r13,-0x10(%rbp) // 跳轉到Method::_from_interpreted_entry處執行 0x00007fffe102217a: jmpq *0x58(%rbx)
理解如上代碼時需要知道vtable方法分派以及vtable在InstanceKlass中的布局,這在《深入剖析Java虛擬機:源碼剖析與實例詳解》一書中詳細介紹過,這里不再介紹。
跳轉到Method::_from_interpretered_entry保存的例程處執行,也就是以解釋執行運行invokevirtual字節碼指令調用的目標方法,關於Method::_from_interpretered_entry保存的例程的邏輯在第6篇、第7篇、第8篇中詳細介紹過,這里不再介紹。
如上的匯編語句 mov 0x1b8(%rax,%rbx,8),%rbx 是通過調用調用lookup_virtual_method()函數生成的,此函數將vtable_entry_addr加載到%rbx中,實現如下:
void MacroAssembler::lookup_virtual_method(Register recv_klass, RegisterOrConstant vtable_index, Register method_result) { const int base = InstanceKlass::vtable_start_offset() * wordSize; Address vtable_entry_addr(recv_klass, vtable_index, Address::times_ptr, base + vtableEntry::method_offset_in_bytes()); movptr(method_result, vtable_entry_addr); }
其中的vtable_index取的就是ConstantPoolCacheEntry::_f2屬性的值。
最后還要說一下,如上生成的一些匯編代碼中省略了統計相關的執行邏輯,這里統計相關的代碼也是非常重要的,它會輔助進行編譯,所以后面我們還會介紹這些統計相關的邏輯。
參考文章:Java字節碼里的invoke操作&&編譯時的靜態綁定與動態綁定
推薦閱讀:
第2篇-JVM虛擬機這樣來調用Java主類的main()方法
第13篇-通過InterpreterCodelet存儲機器指令片段
第20篇-加載與存儲指令之ldc與_fast_aldc指令(2)
第21篇-加載與存儲指令之iload、_fast_iload等(3)