本文由HeapDump效能社群首席講師鳩摩(馬智)授權整理髮布
第29篇-呼叫Java主類的main()方法
前面已經寫了許多篇介紹位元組碼指令對應的彙編程式碼執行邏輯,還有一些位元組碼指令對應的彙編程式碼邏輯沒有介紹,這些指令包括方法呼叫指令、同步指令、異常丟擲指令,這些指令的彙編程式碼實現邏輯比較複雜,所以後面在介紹到方法呼叫、同步和異常處理的知識點時,會通過大篇幅的文章進行詳細介紹!
在第1篇中大概介紹過Java中主類方法main()的呼叫過程,這一篇介紹的詳細一點,大概的呼叫過程如下圖所示。
其中淺紅色的函式由主執行緒執行,而另外的淺綠色部分由另外一個執行緒執行,淺綠色的執行緒最終也會負責執行Java主類中的main()方法。在JavaMain()函式中呼叫LoadMainClass()函式載入Java主類。接著在JavaMain()函式中有如下呼叫:
原始碼位置:openjdk/jdk/src/share/bin/java.c
mainID = (*env)->GetStaticMethodID(
env,
mainClass,
"main",
"([Ljava/lang/String;)V");
env為JNIEnv*型別。呼叫JNIEnv型別中定義的GetStaticMethodID()函式獲取Java主類中main()方法的方法唯一ID,呼叫GetStaticMethodID()函式就是呼叫jni_GetStaticMethodID()函式,此函式的實現如下:
原始碼位置:openjdk/hotspot/src/share/vm/prims/jni.cpp
JNI_ENTRY(jmethodID, jni_GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig))
jmethodID ret = get_method_id(env, clazz, name, sig, true, thread);
return ret;
JNI_END
static jmethodID get_method_id(
JNIEnv *env,
jclass clazz,
const char *name_str,
const char *sig,
bool is_static,
TRAPS
){
const char *name_to_probe = (name_str == NULL)
? vmSymbols::object_initializer_name()->as_C_string()
: name_str;
TempNewSymbol name = SymbolTable::probe(name_to_probe, (int)strlen(name_to_probe));
TempNewSymbol signature = SymbolTable::probe(sig, (int)strlen(sig));
KlassHandle klass(THREAD,java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz)));
// 保證java.lang.Class類已經初始化完成
klass()->initialize(CHECK_NULL);
Method* m;
if ( name == vmSymbols::object_initializer_name() || 查詢的是<init>方法
name == vmSymbols::class_initializer_name() ) { 查詢的是<clinit>方法
// 因為要查詢的是建構函式,建構函式沒有繼承特性,所以當前類找不到時不向父類中繼續查詢
if (klass->oop_is_instance()) {
// find_method()函式不會向上查詢
m = InstanceKlass::cast(klass())->find_method(name, signature);
} else {
m = NULL;
}
} else {
// lookup_method()函式會向上查詢
m = klass->lookup_method(name, signature);
if (m == NULL && klass->oop_is_instance()) {
m = InstanceKlass::cast(klass())->lookup_method_in_ordered_interfaces(name, signature);
}
}
return m->jmethod_id();
}
獲取Java類中main()方法的jmethod_id。
原始碼位置:method.hpp
// Get this method's jmethodID -- allocate if it doesn't exist
jmethodID jmethod_id() {
methodHandle this_h(this);
return InstanceKlass::get_jmethod_id(method_holder(), this_h);
}
呼叫的InstanceKlass::get\_jmethod\_id()函式獲取唯一ID,關於如何獲取或生成ID的過程這裡不再詳細介紹,有興趣的自行研究。
在JavaMain()函式中有如下呼叫:
mainArgs = CreateApplicationArgs(env, argv, argc);
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
通過呼叫CallStaticVoidMethod()函式來呼叫Java主類中的main()方法。控制權轉移到Java主類中的main()方法之中。呼叫CallStaticVoidMethod()函式就是呼叫jni_CallStaticVoidMethod()函式,此函式的實現如下:
原始碼位置:openjdk/hotspot/src/share/vm/prims/jni.cpp
JNI_ENTRY(void, jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...))
va_list args;
va_start(args, methodID);
JavaValue jvalue(T_VOID);
JNI_ArgumentPusherVaArg ap(methodID, args);
jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK);
va_end(args);
JNI_END
將傳給Java方法的引數以C的可變長度引數傳入後,使用JNI_ArgumentPusherVaArg例項ap是將其封裝起來。JNI_ArgumentPusherVaArg類的繼承體系如下:
JNI_ArgumentPusherVaArg->JNI_ArgumentPusher->SignatureIterator
呼叫的jni\_invoke\_static()函式的實現如下:
// 通過jni的方式呼叫Java靜態方法
static void jni_invoke_static(
JNIEnv *env,
JavaValue* result,
jobject receiver,
JNICallType call_type,
jmethodID method_id,
JNI_ArgumentPusher *args,
TRAPS
){
Method* m = Method::resolve_jmethod_id(method_id);
methodHandle method(THREAD, m);
ResourceMark rm(THREAD);
int number_of_parameters = method->size_of_parameters();
// 這裡進一步將要傳給Java的引數轉換為JavaCallArguments物件傳下去
JavaCallArguments java_args(number_of_parameters);
args->set_java_argument_object(&java_args);
// Fill out(填,填寫) JavaCallArguments object
Fingerprinter fp = Fingerprinter(method);
uint64_t x = fp.fingerprint();
args->iterate(x);
// Initialize result type
BasicType bt = args->get_ret_type();
result->set_type(bt);
// Invoke the method. Result is returned as oop.
JavaCalls::call(result, method, &java_args, CHECK);
// Convert result
if (
result->get_type() == T_OBJECT ||
result->get_type() == T_ARRAY
) {
oop tmp = (oop) result->get_jobject();
jobject jobj = JNIHandles::make_local(env,tmp);
result->set_jobject(jobj);
}
}
通過JavaCalls::call()函式來呼叫Java主類的main()方法。關於JavaCalls::call()函式大家應該不會陌生,這個函式是怎麼建立Java棧幀以及找到Java方法入口在之前已經詳細介紹過,這裡不再介紹。
第30篇-解釋執行main()方法小例項
我們在介紹完一些常用位元組碼指令的彙編程式碼執行邏輯後,基本看到一個main()方法從開始呼叫、棧幀建立、位元組碼執行的整個邏輯了,但是方法退棧、同步方法以及異常丟擲等知識點還沒有介紹,我們這裡只舉個最簡單的例子,可以幫助大家回顧一下之前那麼多篇文章所學到的內容。
在第7篇詳細介紹過為Java方法建立的棧幀,如下圖所示。
呼叫完generate\_fixed\_frame()函式後一些暫存器中儲存的值如下:
rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地變數表第1個引數的地址
現在我們舉一個例子,來完整的走一下解釋執行的過程。這個例子如下:
package com.classloading;
public class Test {
public static void main(String[] args) {
int i = 0;
i = i++;
}
}
通過javap -verbose Test.class命令反編譯後的位元組碼檔案內容如下:
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/classloading/Test
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 SourceFile
#11 = Utf8 Test.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 com/classloading/Test
#14 = Utf8 java/lang/Object
{
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: iconst_0
1: istore_1
2: return
}
如上例項對應的棧幀狀態如下圖所示。
現在我們就以解釋執行的方式執行main()方法中的位元組碼。由於是從虛擬機器呼叫過來的,而呼叫完generate\_fixed\_frame()函式後一些暫存器中儲存的值並沒有涉及到棧頂快取,所以需要從iconst\_0這個位元組碼指令的vtos入口進入,然後找到iconst\_0這個位元組碼指令對應的機器指令片段。
現在回顧一下位元組碼分派的邏輯,在generate\_normal\_entry()函式中會呼叫generate\_fixed\_frame()函式為Java方法的執行生成對應的棧幀,接下來還會呼叫dispatch_next()函式執行Java方法的位元組碼,首次獲取位元組碼時的彙編如下:
// 在generate_fixed_frame()方法中已經讓%r13儲存了bcp
movzbl 0x0(%r13),%ebx // %ebx中儲存的是位元組碼的操作碼
// $0x7ffff73ba4a0這個地址指向的是對應state狀態下的一維陣列,長度為256
movabs $0x7ffff73ba4a0,%r10
// 注意%r10中儲存的是常量,根據計算公式%r10+%rbx*8來獲取指向儲存入口地址的地址,
// 通過*(%r10+%rbx*8)獲取到入口地址,然後跳轉到入口地址執行
jmpq *(%r10,%rbx,8)
注意如上的$0x7ffff73ba4a0這個常量值已經表示了棧頂快取狀態為vtos下的一維陣列首地址。而在首次進行方法的位元組碼分派時,通過0x0(%r13)即可取出位元組碼對應的Opcode,使用這個Opcode可定位到iconst_0的入口地址。
%r10指向的是對應棧頂快取狀態state下的一維陣列,長度為256,其中儲存的值為Opcode,這在第8篇詳細介紹過,示意圖如下圖所示。
現在就是看入口為vtos,出口為itos的iconst_0所要執行的彙編程式碼了,如下:
...
// vtos入口
mov $0x1,%eax
...
// iconst_0對應的彙編程式碼
xor %eax,%eax
彙編指令足夠簡單,最後將值儲存到了%eax中,所以也就是棧頂快取的出口狀態為itos。
上圖紫色的部分是本地變數表,由於本地變數表的大小為2,所以我畫了2個方格表示slot。
執行下一個位元組碼指令istore_1,也會執行位元組碼分派相關的邏輯。這裡需要提醒下,其實之前在介紹位元組碼指令對應的彙編時,只關注了位元組碼指令本身的執行邏輯,其實在為每個位元組碼指令生成機器指令時,一般都會為這些位元組碼指令生成3部分機器指令片段:
(1)不同棧頂狀態對應的入口執行邏輯;
(2)位元組碼指令本身需要執行的邏輯;
(3)分派到下一個位元組碼指令的邏輯。
對於位元組碼指令模板定義中,如果flags中指令有disp,那麼這些指令自己會含有分派的邏輯,如goto、ireturn、tableswitch、lookupswitch、jsr等。由於我們的指令是iconst_0,所以會為這個位元組碼指令生成分派邏輯,生成的邏輯如下:
movzbl 0x1(%r13),%ebx // %ebx中儲存的是位元組碼的操作碼
movabs itos對應的一維陣列的首地址,%r10
jmpq *(%r10,%rbx,8)
需要注意的是,如果要讓%ebx中儲存istore\_1的Opcode,則%r13需要加上iconst\_0指令的長度,即1。由於iconst\_0執行後的出口棧頂快取為itos,所以要找到入口狀態為itos,而Opcode為istore\_1的機器指令片段執行。指令片段如下:
mov %eax,-0x8(%r14)
程式碼將棧頂的值%eax儲存到本地變數表下標索引為1的位置處。通過%r14很容易定位到本地變數表的位置,執行完成後的棧狀態如下圖所示。
執行iconst\_0和istore\_1時,整個過程沒有向表示式棧(上圖中sp/rsp開始以下的部分就是表示式棧)中壓入0,實際上如果沒有棧頂快取的優化,應該將0壓入棧頂,然後彈出棧頂儲存到區域性變數表,但是有了棧頂快取後,沒有壓棧操作,也就有彈棧操作,所以能極大的提高程式的執行效率。
return指令判斷的邏輯比較多,主要是因為有些方法可能有synchronized關鍵字,所以會在方法棧中儲存鎖相關的資訊,而在return返回時,退棧要釋放鎖。不過我們現在只看針對本例項要執行的部分程式碼,如下:
// 將JavaThread::do_not_unlock_if_synchronized屬性儲存到%dl中
0x00007fffe101b770: mov 0x2ad(%r15),%dl
// 重置JavaThread::do_not_unlock_if_synchronized屬性值為false
0x00007fffe101b777: movb $0x0,0x2ad(%r15)
// 將Method*載入到%rbx中
0x00007fffe101b77f: mov -0x18(%rbp),%rbx
// 將Method::_access_flags載入到%ecx中
0x00007fffe101b783: mov 0x28(%rbx),%ecx
// 檢查Method::flags是否包含JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test $0x20,%ecx
// 如果方法不是同步方法,跳轉到----unlocked----
0x00007fffe101b78c: je 0x00007fffe101b970
main()方法為非同步方法,所以跳轉到unlocked處執行,在unlocked處執行的邏輯中會執行一些釋放鎖的邏輯,對於我們本例項來說這不重要,我們直接看退棧的操作,如下:
// 將-0x8(%rbp)處儲存的old stack pointer(saved rsp)取出來放到%rbx中
0x00007fffe101bac7: mov -0x8(%rbp),%rbx
// 移除棧幀
// leave指令相當於:
// mov %rbp, %rsp
// pop %rbp
0x00007fffe101bacb: leaveq
// 將返回地址彈出到%r13中
0x00007fffe101bacc: pop %r13
// 設定%rsp為呼叫者的棧頂值
0x00007fffe101bace: mov %rbx,%rsp
0x00007fffe101bad1: jmpq *%r13
這個彙編不難,這裡不再繼續介紹。退棧後的棧狀態如下圖所示。
這就完全回到了呼叫Java方法之前的棧狀態,接下來如何退出如上棧幀並結束方法呼叫就是C++語言的事兒了。
第31篇-方法呼叫指令之invokevirtual
invokevirtual位元組碼指令的模板定義如下:
def(Bytecodes::_invokevirtual , ubcp|disp|clvm|____, vtos, vtos, invokevirtual , f2_byte );
生成函式為invokevirtual,傳遞的引數為f2_byte,也就是2,如果為2時,ConstantPoolCacheEntry::indices中取[b2,b1,original constant pool 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、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、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屬性的值。
最後還要說一下,如上生成的一些彙編程式碼中省略了統計相關的執行邏輯,這裡統計相關的程式碼也是非常重要的,它會輔助進行編譯,所以後面我們還會介紹這些統計相關的邏輯。
第32篇-解析interfacevirtual位元組碼指令
在前面介紹invokevirtual指令時,如果判斷出ConstantPoolCacheEntry中的\_indices欄位的\_f2屬性的值為空,則認為呼叫的目標方法沒有連線,也就是沒有向ConstantPoolCacheEntry中儲存呼叫方法的相關資訊,需要呼叫InterpreterRuntime::resolve_invoke()函式進行方法連線,這個函式的實現比較多,我們分幾部分檢視:
InterpreterRuntime::resolve_invoke()函式第1部分:
Handle receiver(thread, NULL);
if (bytecode == Bytecodes::_invokevirtual || bytecode == Bytecodes::_invokeinterface) {
ResourceMark rm(thread);
// 呼叫method()函式從當前的棧幀中獲取到需要執行的方法
Method* m1 = method(thread);
methodHandle m (thread, m1);
// 呼叫bci()函式從當前的棧幀中獲取需要執行的方法的位元組碼索引
int i1 = bci(thread);
Bytecode_invoke call(m, i1);
// 當前需要執行的方法的簽名
Symbol* signature = call.signature();
frame fm = thread->last_frame();
oop x = fm.interpreter_callee_receiver(signature);
receiver = Handle(thread,x);
}
當位元組碼為invokevirtual或invokeinterface這樣的動態分派位元組碼時,執行如上的邏輯。獲取到了receiver變數的值。接著看實現,如下:
InterpreterRuntime::resolve_invoke()函式第2部分:
CallInfo info;
constantPoolHandle pool(thread, method(thread)->constants());
{
JvmtiHideSingleStepping jhss(thread);
int cpcacheindex = get_index_u2_cpcache(thread, bytecode);
LinkResolver::resolve_invoke(info, receiver, pool,cpcacheindex, bytecode, CHECK);
...
}
// 如果已經向ConstantPoolCacheEntry中更新了呼叫的相關資訊則直接返回
if (already_resolved(thread))
return;
根據儲存在當前棧中的bcp來獲取位元組碼指令的運算元,這個運算元通常就是常量池快取項索引。然後呼叫LinkResolver::resolve\_invoke()函式進行方法連線。 這個函式會間接呼叫LinkResolver::resolve\_invokevirtual()函式,實現如下:
void LinkResolver::resolve_invokevirtual(
CallInfo& result,
Handle recv,
constantPoolHandle pool,
int index,
TRAPS
){
KlassHandle resolved_klass;
Symbol* method_name = NULL;
Symbol* method_signature = NULL;
KlassHandle current_klass;
resolve_pool(resolved_klass, method_name, method_signature, current_klass, pool, index, CHECK);
KlassHandle recvrKlass(THREAD, recv.is_null() ? (Klass*)NULL : recv->klass());
resolve_virtual_call(result, recv, recvrKlass, resolved_klass, method_name, method_signature, current_klass, true, true, CHECK);
}
其中會呼叫resolve\_pool()和resolve\_vritual_call()函式分別連線常量池和方法呼叫指令。呼叫會涉及到的相關函式大概如下圖所示。
下面介紹resolve\_pool()和resolve\_virtual_call()函式及其呼叫的相關函式的實現。
01 resolve_pool()函式
呼叫的resolve_pool()函式會呼叫一些函式,如下圖所示。
每次呼叫LinkResolver::resolve\_pool()函式時不一定會按如上的函式呼叫鏈執行,但是當類還沒有解析時,通常會呼叫SystemDictionary::resolve\_or_fail()函式進行解析,最終會獲取到指向Klass例項的指標,最終將這個類更新到常量池中。
resolve_pool()函式的實現如下:
void LinkResolver::resolve_pool(
KlassHandle& resolved_klass,
Symbol*& method_name,
Symbol*& method_signature,
KlassHandle& current_klass,
constantPoolHandle pool,
int index,
TRAPS
) {
resolve_klass(resolved_klass, pool, index, CHECK);
method_name = pool->name_ref_at(index);
method_signature = pool->signature_ref_at(index);
current_klass = KlassHandle(THREAD, pool->pool_holder());
}
其中的index為常量池快取項的索引。resolved\_klass參數列示需要進行解析的類(解析是在類生成周期中連線相關的部分,所以我們之前有時候會稱為連線,其實具體來說是解析的意思),而current\_klass為當前擁有常量池的類,由於傳遞引數時是C++的引用傳遞,所以同值會直接改變變數的值,呼叫者中的值也會隨著改變。
呼叫resolve_klass()函式進行類解析,一般來說,類解析會在解釋常量池項時就會進行,這在《深入剖析Java虛擬機器:原始碼剖析與例項詳解(基礎卷)》一書中介紹過,這裡需要再說一下。
呼叫的resolve_klass()函式及相關函式的實現如下:
void LinkResolver::resolve_klass(
KlassHandle& result,
constantPoolHandle pool,
int index,
TRAPS
) {
Klass* result_oop = pool->klass_ref_at(index, CHECK);
// 通過引用進行傳遞
result = KlassHandle(THREAD, result_oop);
}
Klass* ConstantPool::klass_ref_at(int which, TRAPS) {
int x = klass_ref_index_at(which);
return klass_at(x, CHECK_NULL);
}
int klass_ref_index_at(int which) {
return impl_klass_ref_index_at(which, false);
}
呼叫的impl\_klass\_ref\_index\_at()函式的實現如下:
int ConstantPool::impl_klass_ref_index_at(int which, bool uncached) {
int i = which;
if (!uncached && cache() != NULL) {
// 從which對應的ConstantPoolCacheEntry項中獲取ConstantPoolIndex
i = remap_instruction_operand_from_cache(which);
}
assert(tag_at(i).is_field_or_method(), "Corrupted constant pool");
// 獲取
jint ref_index = *int_at_addr(i);
// 獲取低16位,那就是class_index
return extract_low_short_from_int(ref_index);
}
根據斷言可知,在原常量池索引的i處的項肯定為JVM\_CONSTANT\_Fieldref、JVM\_CONSTANT\_Methodref或JVM\_CONSTANT\_InterfaceMethodref,這幾項的格式如下:
CONSTANT_Fieldref_info{
u1 tag;
u2 class_index;
u2 name_and_type_index; // 必須是欄位描述符
}
CONSTANT_InterfaceMethodref_info{
u1 tag;
u2 class_index; // 必須是介面
u2 name_and_type_index; // 必須是方法描述符
}
CONSTANT_Methodref_info{
u1 tag;
u2 class_index; // 必須是類
u2 name_and_type_index; // 必須是方法描述符
}
3項的格式都一樣,其中的class\_index索引處的項必須為CONSTANT\_Class\_info結構,表示一個類或介面,當前類欄位或方法是這個類或介面的成員。name\_and\_type\_index索引處必須為CONSTANT\_NameAndType\_info項。
通過呼叫int\_at\_addr()函式和extract\_low\_short\_from\_int()函式獲取class_index的索引值,如果瞭解了常量池記憶體佈局,這裡函式的實現理解起來會很簡單,這裡不再介紹。
在klass\_ref\_at()函式中呼叫klass_at()函式,此函式的實現如下:
Klass* klass_at(int which, TRAPS) {
constantPoolHandle h_this(THREAD, this);
return klass_at_impl(h_this, which, CHECK_NULL);
}
呼叫的klass\_at\_impl()函式的實現如下:
Klass* ConstantPool::klass_at_impl(
constantPoolHandle this_oop,
int which,
TRAPS
) {
CPSlot entry = this_oop->slot_at(which);
if (entry.is_resolved()) { // 已經進行了連線
return entry.get_klass();
}
bool do_resolve = false;
bool in_error = false;
Handle mirror_handle;
Symbol* name = NULL;
Handle loader;
{
MonitorLockerEx ml(this_oop->lock());
if (this_oop->tag_at(which).is_unresolved_klass()) {
if (this_oop->tag_at(which).is_unresolved_klass_in_error()) {
in_error = true;
} else {
do_resolve = true;
name = this_oop->unresolved_klass_at(which);
loader = Handle(THREAD, this_oop->pool_holder()->class_loader());
}
}
} // unlocking constantPool
// 省略當in_error變數的值為true時的處理邏輯
if (do_resolve) {
oop protection_domain = this_oop->pool_holder()->protection_domain();
Handle h_prot (THREAD, protection_domain);
Klass* k_oop = SystemDictionary::resolve_or_fail(name, loader, h_prot, true, THREAD);
KlassHandle k;
if (!HAS_PENDING_EXCEPTION) {
k = KlassHandle(THREAD, k_oop);
mirror_handle = Handle(THREAD, k_oop->java_mirror());
}
if (HAS_PENDING_EXCEPTION) {
...
return 0;
}
if (TraceClassResolution && !k()->oop_is_array()) {
...
} else {
MonitorLockerEx ml(this_oop->lock());
do_resolve = this_oop->tag_at(which).is_unresolved_klass();
if (do_resolve) {
ClassLoaderData* this_key = this_oop->pool_holder()->class_loader_data();
this_key->record_dependency(k(), CHECK_NULL); // Can throw OOM
this_oop->klass_at_put(which, k()); // 注意這裡會更新常量池中儲存的內容,這樣就表示類已經解析完成,下次就不需要重複解析了
}
}
}
entry = this_oop->resolved_klass_at(which);
assert(entry.is_resolved() && entry.get_klass()->is_klass(), "must be resolved at this point");
return entry.get_klass();
}
函式首先呼叫slot\_at()函式獲取常量池中一個slot中儲存的值,然後通過CPSlot來表示這個slot,這個slot中可能儲存的值有2個,分別為指向Symbol例項(因為類名用CONSTANT\_Utf8_info項表示,在虛擬機器內部統一使用Symbol物件表示字串)的指標和指向Klass例項的指標,如果類已經解釋,那麼指標表示的地址的最後一位為0,如果還沒有被解析,那麼地址的最後一位為1。
當沒有解析時,需要呼叫SystemDictionary::resolve\_or\_fail()函式獲取類Klass的例項,然後更新常量池中的資訊,這樣下次就不用重複解析類了。最後返回指向Klass例項的指標即可。
繼續回到LinkResolver::resolve\_pool()函式看接下來的執行邏輯,也就是會獲取JVM\_CONSTANT\_Fieldref、JVM\_CONSTANT\_Methodref或JVM\_CONSTANT\_InterfaceMethodref項中的name\_and\_type\_index,其指向的是CONSTANT\_NameAndType\_info項,格式如下:
CONSTANT_NameAndType_info{
u1 tag;
u2 name_index;
u2 descriptor index;
}
獲取邏輯就是先根據常量池快取項的索引找到原常量池項的索引,然後查詢到CONSTANT\_NameAndType\_info後,獲取到方法名稱和簽名的索引,進而獲取到被呼叫的目標方法的名稱和簽名。這些資訊將在接下來呼叫的resolve\_virtual\_call()函式中使用。
02 resolve\_virtual\_call()函式
resolve\_virtual\_call()函式會呼叫的相關函式如下圖所示。
LinkResolver::resolve\_virtual\_call()的實現如下:
void LinkResolver::resolve_virtual_call(
CallInfo& result,
Handle recv,
KlassHandle receiver_klass,
KlassHandle resolved_klass,
Symbol* method_name,
Symbol* method_signature,
KlassHandle current_klass,
bool check_access,
bool check_null_and_abstract,
TRAPS
) {
methodHandle resolved_method;
linktime_resolve_virtual_method(resolved_method, resolved_klass, method_name, method_signature, current_klass, check_access, CHECK);
runtime_resolve_virtual_method(result, resolved_method, resolved_klass, recv, receiver_klass, check_null_and_abstract, CHECK);
}
首先呼叫LinkResolver::linktime\_resolve\_virtual_method()函式,這個函式會呼叫如下函式:
void LinkResolver::resolve_method(
methodHandle& resolved_method,
KlassHandle resolved_klass,
Symbol* method_name,
Symbol* method_signature,
KlassHandle current_klass,
bool check_access,
bool require_methodref,
TRAPS
) {
// 從解析的類和其父類中查詢方法
lookup_method_in_klasses(resolved_method, resolved_klass, method_name, method_signature, true, false, CHECK);
// 沒有在解析類的繼承體系中查詢到方法
if (resolved_method.is_null()) {
// 從解析類實現的所有介面(包括間接實現的介面)中查詢方法
lookup_method_in_interfaces(resolved_method, resolved_klass, method_name, method_signature, CHECK);
// ...
if (resolved_method.is_null()) {
// 沒有找到對應的方法
...
}
}
// ...
}
如上函式中最主要的就是根據method\_name和method\_signature從resolved\_klass類中找到合適的方法,如果找到就賦值給resolved\_method變數。
呼叫lookup\_method\_in\_klasses()、lookup\_method\_in\_interfaces()等函式進行方法的查詢,這裡暫時不介紹。
下面接著看runtime\_resolve\_virtual_method()函式,這個函式的實現如下:
void LinkResolver::runtime_resolve_virtual_method(
CallInfo& result,
methodHandle resolved_method,
KlassHandle resolved_klass,
Handle recv,
KlassHandle recv_klass,
bool check_null_and_abstract,
TRAPS
) {
int vtable_index = Method::invalid_vtable_index;
methodHandle selected_method;
// 當方法定義在介面中時,表示是miranda方法
if (resolved_method->method_holder()->is_interface()) {
vtable_index = vtable_index_of_interface_method(resolved_klass,resolved_method);
InstanceKlass* inst = InstanceKlass::cast(recv_klass());
selected_method = methodHandle(THREAD, inst->method_at_vtable(vtable_index));
} else {
// 如果走如下的程式碼邏輯,則表示resolved_method不是miranda方法,需要動態分派且肯定有正確的vtable索引
vtable_index = resolved_method->vtable_index();
// 有些方法雖然看起來需要動態分派,但是如果這個方法有final關鍵字時,可進行靜態繫結,所以直接呼叫即可
// final方法其實不會放到vtable中,除非final方法覆寫了父類中的方法
if (vtable_index == Method::nonvirtual_vtable_index) {
selected_method = resolved_method;
} else {
// 根據vtable和vtable_index以及inst進行方法的動態分派
InstanceKlass* inst = (InstanceKlass*)recv_klass();
selected_method = methodHandle(THREAD, inst->method_at_vtable(vtable_index));
}
}
// setup result resolve的型別為CallInfo,為CallInfo設定了連線後的相關資訊
result.set_virtual(resolved_klass, recv_klass, resolved_method, selected_method, vtable_index, CHECK);
}
當為miranda方法時,呼叫 LinkResolver::vtable\_index\_of\_interface\_method()函式查詢;當為final方法時,因為final方法不可能被子類覆寫,所以resolved\_method就是目標呼叫方法;除去前面的2種情況後,剩下的方法就需要結合vtable和vtable\_index進行動態分派了。
如上函式將查詢到呼叫時需要的所有資訊並儲存到CallInfo型別的result變數中。
在獲取到呼叫時的所有資訊並儲存到CallInfo中後,就可以根據info中相關資訊填充ConstantPoolCacheEntry。我們回看InterpreterRuntime::resolve_invoke()函式的執行邏輯。
InterpreterRuntime::resolve_invoke()函式第2部分:
switch (info.call_kind()) {
case CallInfo::direct_call: // 直接呼叫
cache_entry(thread)->set_direct_call(
bytecode,
info.resolved_method());
break;
case CallInfo::vtable_call: // vtable分派
cache_entry(thread)->set_vtable_call(
bytecode,
info.resolved_method(),
info.vtable_index());
break;
case CallInfo::itable_call: // itable分派
cache_entry(thread)->set_itable_call(
bytecode,
info.resolved_method(),
info.itable_index());
break;
default: ShouldNotReachHere();
}
無論直接呼叫,還是vtable和itable動態分派,都會在方法解析完成後將相關的資訊儲存到常量池快取項中。呼叫cache\_entry()函式獲取對應的ConstantPoolCacheEntry項,然後呼叫set\_vtable_call()函式,此函式會呼叫如下函式更新ConstantPoolCacheEntry項中的資訊,如下:
void ConstantPoolCacheEntry::set_direct_or_vtable_call(
Bytecodes::Code invoke_code,
methodHandle method,
int vtable_index
) {
bool is_vtable_call = (vtable_index >= 0); // FIXME: split this method on this boolean
int byte_no = -1;
bool change_to_virtual = false;
switch (invoke_code) {
case Bytecodes::_invokeinterface:
change_to_virtual = true;
// ...
// 可以看到,通過_invokevirtual指令時,並不一定都是動態分發,也有可能是靜態繫結
case Bytecodes::_invokevirtual: // 當前已經在ConstantPoolCacheEntry類中了
{
if (!is_vtable_call) {
assert(method->can_be_statically_bound(), "");
// set_f2_as_vfinal_method checks if is_vfinal flag is true.
set_method_flags(as_TosState(method->result_type()),
( 1 << is_vfinal_shift) |
((method->is_final_method() ? 1 : 0) << is_final_shift) |
((change_to_virtual ? 1 : 0) << is_forced_virtual_shift), // 在介面中呼叫Object中定義的方法
method()->size_of_parameters());
set_f2_as_vfinal_method(method());
} else {
// 執行這裡的邏輯時,表示方法是非靜態繫結的非final方法,需要動態分派,則vtable_index的值肯定大於等於0
set_method_flags(as_TosState(method->result_type()),
((change_to_virtual ? 1 : 0) << is_forced_virtual_shift),
method()->size_of_parameters());
// 對於動態分發來說,ConstantPoolCacheEntry::_f2中儲存的是vtable_index
set_f2(vtable_index);
}
byte_no = 2;
break;
}
// ...
}
if (byte_no == 1) {
// invoke_code為非invokevirtual和非invokeinterface位元組碼指令
set_bytecode_1(invoke_code);
} else if (byte_no == 2) {
if (change_to_virtual) {
if (method->is_public())
set_bytecode_1(invoke_code);
} else {
assert(invoke_code == Bytecodes::_invokevirtual, "");
}
// set up for invokevirtual, even if linking for invokeinterface also:
set_bytecode_2(Bytecodes::_invokevirtual);
}
}
連線完成後ConstantPoolCacheEntry中的各個項如下圖所示。
所以對於invokevirtual來說,通過vtable進行方法的分發,在ConstantPoolCacheEntry中,\_f1欄位沒有使用,而對\_f2欄位來說,如果呼叫的是非final的virtual方法,則儲存的是目標方法在vtable中的索引編號,如果是virtual final方法,則_f2欄位直接指向目標方法的Method例項。
第33篇-方法呼叫指令之invokeinterface
invokevirtual位元組碼指令的模板定義如下:
def(Bytecodes::_invokeinterface , ubcp|disp|clvm|____, vtos, vtos, invokeinterface , f1_byte );
可以看到指令的生成函式為TemplateTable::invokeinterface(),在這個函式中首先會呼叫TemplateTable::prepare\_invoke()函式,TemplateTable::prepare\_invoke()函式生成的彙編程式碼如下:
第1部分
0x00007fffe1022610: mov %r13,-0x38(%rbp)
0x00007fffe1022614: movzwl 0x1(%r13),%edx
0x00007fffe1022619: mov -0x28(%rbp),%rcx
0x00007fffe102261d: shl $0x2,%edx
// 獲取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices
0x00007fffe1022620: mov 0x10(%rcx,%rdx,8),%ebx
// 獲取ConstantPoolCacheEntry中indices[b2,b1,constant pool index]中的b1
// 如果已經連線,那這個b1應該等於185,也就是invokeinterface指令的操作碼
0x00007fffe1022624: shr $0x10,%ebx
0x00007fffe1022627: and $0xff,%ebx
0x00007fffe102262d: cmp $0xb9,%ebx
// 如果invokeinterface已經連線就跳轉到----resolved----
0x00007fffe1022633: je 0x00007fffe10226d2
彙編程式碼的判斷邏輯與invokevirutal一致,這裡不在過多解釋。
第2部分
由於方法還沒有解析,所以需要設定ConstantPoolCacheEntry中的資訊,這樣再一次呼叫時就不需要重新找呼叫相關的資訊了。生成的彙編如下:
// 執行如下彙編程式碼時,表示invokeinterface指令還沒有連線,也就是ConstantPoolCacheEntry中
// 還沒有儲存呼叫相關的資訊
// 通過呼叫call_VM()函式生成如下彙編,通過這些彙編
// 呼叫InterpreterRuntime::resolve_invoke()函式
// 將bytecode儲存到%ebx中
0x00007fffe1022639: mov $0xb9,%ebx
// 通過MacroAssembler::call_VM()來呼叫InterpreterRuntime::resolve_invoke()
0x00007fffe102263e: callq 0x00007fffe1022648
0x00007fffe1022643: jmpq 0x00007fffe10226c6
0x00007fffe1022648: mov %rbx,%rsi
0x00007fffe102264b: lea 0x8(%rsp),%rax
0x00007fffe1022650: mov %r13,-0x38(%rbp)
0x00007fffe1022654: mov %r15,%rdi
0x00007fffe1022657: mov %rbp,0x200(%r15)
0x00007fffe102265e: mov %rax,0x1f0(%r15)
0x00007fffe1022665: test $0xf,%esp
0x00007fffe102266b: je 0x00007fffe1022683
0x00007fffe1022671: sub $0x8,%rsp
0x00007fffe1022675: callq 0x00007ffff66ae13a
0x00007fffe102267a: add $0x8,%rsp
0x00007fffe102267e: jmpq 0x00007fffe1022688
0x00007fffe1022683: callq 0x00007ffff66ae13a
0x00007fffe1022688: movabs $0x0,%r10
0x00007fffe1022692: mov %r10,0x1f0(%r15)
0x00007fffe1022699: movabs $0x0,%r10
0x00007fffe10226a3: mov %r10,0x200(%r15)
0x00007fffe10226aa: cmpq $0x0,0x8(%r15)
0x00007fffe10226b2: je 0x00007fffe10226bd
0x00007fffe10226b8: jmpq 0x00007fffe1000420
0x00007fffe10226bd: mov -0x38(%rbp),%r13
0x00007fffe10226c1: mov -0x30(%rbp),%r14
0x00007fffe10226c5: retq
// 結束MacroAssembler::call_VM()函式
// 將invokeinterface x中的x載入到%edx中
0x00007fffe10226c6: movzwl 0x1(%r13),%edx
// 將ConstantPoolCache的首地址儲存到%rcx中
0x00007fffe10226cb: mov -0x28(%rbp),%rcx
// %edx中儲存的是ConstantPoolCacheEntry項的索引,轉換為位元組
// 偏移,因為一個ConstantPoolCacheEntry項佔用4個字
0x00007fffe10226cf: shl $0x2,%edx
與invokevirtual的實現類似,這裡仍然在方法沒有解釋時呼叫InterpreterRuntime::resolve\_invoke()函式進行方法解析,後面我們也詳細介紹一下InterpreterRuntime::resolve\_invoke()函式的實現。
在呼叫完resolve\_invoke()函式後,會將呼叫相信的資訊儲存到CallInfo例項info中。所以在呼叫的InterpreterRuntime::resolve\_invoke()函式的最後會有如下的實現:
switch (info.call_kind()) {
case CallInfo::direct_call: // 直接呼叫
cache_entry(thread)->set_direct_call(
bytecode,
info.resolved_method());
break;
case CallInfo::vtable_call: // vtable分派
cache_entry(thread)->set_vtable_call(
bytecode,
info.resolved_method(),
info.vtable_index());
break;
case CallInfo::itable_call: // itable分派
cache_entry(thread)->set_itable_call(
bytecode,
info.resolved_method(),
info.itable_index());
break;
default: ShouldNotReachHere();
}
之前已經介紹過vtable分派,現在看一下itable分派。
當為itable分派時,會呼叫set\_itable\_call()函式設定ConstantPoolCacheEntry中的相關資訊,這個函式的實現如下:
void ConstantPoolCacheEntry::set_itable_call(
Bytecodes::Code invoke_code,
methodHandle method,
int index
) {
InstanceKlass* interf = method->method_holder();
// interf一定是介面,method一定是非final方法
set_f1(interf); // 對於itable,則_f1為InstanceKlass
set_f2(index);
set_method_flags(as_TosState(method->result_type()),
0, // no option bits
method()->size_of_parameters());
set_bytecode_1(Bytecodes::_invokeinterface);
}
ConstantPoolCacheEntry中儲存的資訊為:
- bytecode儲存到了_f2欄位上,這樣當這個欄位有值時表示已經對此方法完成了解析;
- \_f1欄位儲存宣告方法的介面類,也就是\_f1是指向表示介面的Klass例項的指標;
- \_f2表示\_f1介面類對應的方法表中的索引,如果是final方法,則儲存指向Method例項的指標。
解析完成後ConstantPoolCacheEntry中的各個項如下圖所示。
第3部分
如果invokeinterface位元組碼指令已經解析,則直接跳轉到resolved執行,否則呼叫resolve_invoke進行解析,解析完成後也會接著執行resolved處的邏輯,如下:
// **** resolved ****
// resolved的定義點,到這裡說明invokeinterface位元組碼已經連線
// 執行完如上彙編後暫存器的值如下:
// %edx:ConstantPoolCacheEntry index
// %rcx:ConstantPoolCache
// 獲取到ConstantPoolCacheEntry::_f1
// 在計算時,因為ConstantPoolCacheEntry在ConstantPoolCache
// 之後儲存,所以ConstantPoolCache為0x10,而
// _f1還要偏移0x8,這樣總偏移就是0x18
0x00007fffe10226d2: mov 0x18(%rcx,%rdx,8),%rax
// 獲取ConstantPoolCacheEntry::_f2屬性
0x00007fffe10226d7: mov 0x20(%rcx,%rdx,8),%rbx
// 獲取ConstantPoolCacheEntry::_flags屬性
0x00007fffe10226dc: mov 0x28(%rcx,%rdx,8),%edx
// 執行如上彙編後暫存器的值如下:
// %rax:ConstantPoolCacheEntry::_f1
// %rbx:ConstantPoolCacheEntry::_f2
// %edx:ConstantPoolCacheEntry::_flags
// 將flags移動到ecx中
0x00007fffe10226e0: mov %edx,%ecx
// 從ConstantPoolCacheEntry::_flags中獲取引數大小
0x00007fffe10226e2: and $0xff,%ecx
// 讓%rcx指向recv
0x00007fffe10226e8: mov -0x8(%rsp,%rcx,8),%rcx
// 暫時用%r13d儲存ConstantPoolCacheEntry::_flags屬性
0x00007fffe10226ed: mov %edx,%r13d
// 從_flags的高4位儲存的TosState中獲取方法返回型別
0x00007fffe10226f0: shr $0x1c,%edx
// 將TemplateInterpreter::invoke_return_entry地址儲存到%r10
0x00007fffe10226f3: movabs $0x7ffff73b63e0,%r10
// %rdx儲存的是方法返回型別,計算返回地址
// 因為TemplateInterpreter::invoke_return_entry是陣列,
// 所以要找到對應return type的入口地址
0x00007fffe10226fd: mov (%r10,%rdx,8),%rdx
// 獲取結果處理函式TemplateInterpreter::invoke_return_entry的地址並壓入棧中
0x00007fffe1022701: push %rdx
// 恢復ConstantPoolCacheEntry::_flags中%edx
0x00007fffe1022702: mov %r13d,%edx
// 還原bcp
0x00007fffe1022705: mov -0x38(%rbp),%r13
在TemplateTable::invokeinterface()函式中首先會呼叫prepare_invoke()函式,上面的彙編就是由這個函式生成的。呼叫完後各個暫存器的值如下:
rax: interface klass (from f1)
rbx: itable index (from f2)
rcx: receiver
rdx: flags
然後接著執行TemplateTable::invokeinterface()函式生成的彙編片段,如下:
第4部分
// 將ConstantPoolCacheEntry::_flags的值儲存到%r14d中
0x00007fffe1022709: mov %edx,%r14d
// 檢測一下_flags中是否含有is_forced_virtual_shift標識,如果有,
// 表示呼叫的是Object類中的方法,需要通過vtable進行動態分派
0x00007fffe102270c: and $0x800000,%r14d
0x00007fffe1022713: je 0x00007fffe1022812 // 跳轉到----notMethod----
// ConstantPoolCacheEntry::_flags儲存到%eax
0x00007fffe1022719: mov %edx,%eax
// 測試呼叫的方法是否為final
0x00007fffe102271b: and $0x100000,%eax
0x00007fffe1022721: je 0x00007fffe1022755 // 如果為非final方法,則跳轉到----notFinal----
// 下面彙編程式碼是對final方法的處理
// 對於final方法來說,rbx中儲存的是Method*,也就是ConstantPoolCacheEntry::_f2指向Method*
// 跳轉到Method::from_interpreted處執行即可
0x00007fffe1022727: cmp (%rcx),%rax
// ... 省略統計相關的程式碼
// 設定呼叫者棧頂並儲存
0x00007fffe102274e: mov %r13,-0x10(%rbp)
// 跳轉到Method::_from_interpreted_entry
0x00007fffe1022752: jmpq *0x58(%rbx) // 呼叫final方法
// **** notFinal ****
// 呼叫load_klass()函式生成如下2句彙編
// 檢視recv這個oop對應的Klass,儲存到%eax中
0x00007fffe1022755: mov 0x8(%rcx),%eax
// 呼叫decode_klass_not_null()函式生成的彙編
0x00007fffe1022758: shl $0x3,%rax
// 省略統計相關的程式碼
// 呼叫lookup_virtual_method()函式生成如下這一句彙編
0x00007fffe10227fe: mov 0x1b8(%rax,%rbx,8),%rbx
// 設定呼叫者棧頂並儲存
0x00007fffe1022806: lea 0x8(%rsp),%r13
0x00007fffe102280b: mov %r13,-0x10(%rbp)
// 跳轉到Method::_from_interpreted_entry
0x00007fffe102280f: jmpq *0x58(%rbx)
如上彙編包含了對final和非final方法的分派邏輯。對於final方法來說,由於ConstantPoolCacheEntry::_f2中儲存的就是指向被呼叫的Method例項,所以非常簡單;對於非final方法來說,需要通過vtable實現動態分派。分派的關鍵一個彙編語句如下:
mov 0x1b8(%rax,%rbx,8),%rbx
需要提示的是,只有少量的方法可能才會走這個邏輯進行vtable的動態分派,如呼叫Object類中的方法。
如果跳轉到notMethod後,那就需要通過itable進行方法的動態分派了,我們看一下這部分的實現邏輯:
第5部分
// **** notMethod ****
// 讓%r14指向本地變數表
0x00007fffe1022812: mov -0x30(%rbp),%r14
// %rcx中儲存的是receiver,%edx中儲存的是Klass
0x00007fffe1022816: mov 0x8(%rcx),%edx
// LogKlassAlignmentInBytes=0x03,進行對齊處理
0x00007fffe1022819: shl $0x3,%rdx
// 如下程式碼是呼叫如下函式生成的:
__ lookup_interface_method(rdx, // inputs: rec. class
rax, // inputs: interface
rbx, // inputs: itable index
rbx, // outputs: method
r13, // outputs: scan temp. reg
no_such_interface);
// 獲取vtable的起始地址
// %rdx中儲存的是recv.Klass,獲取Klass中
// vtable_length屬性的值
0x00007fffe10228c1: mov 0x118(%rdx),%r13d
// %rdx:recv.Klass,%r13為vtable_length,
// 最後r13指向第一個itableOffsetEntry
// 加一個常量0x1b8是因為vtable之前是InstanceKlass
0x00007fffe10228c8: lea 0x1b8(%rdx,%r13,8),%r13
0x00007fffe10228d0: lea (%rdx,%rbx,8),%rdx
// 獲取itableOffsetEntry::_interface並與%rax比較,%rax中儲存的是要查詢的介面
0x00007fffe10228d4: mov 0x0(%r13),%rbx
0x00007fffe10228d8: cmp %rbx,%rax
// 如果相等,則直接跳轉到---- found_method ----
0x00007fffe10228db: je 0x00007fffe10228f3
// **** search ****
// 檢測%rbx中的值是否為NULL,如果為NULL,
// 那就說明receiver沒有實現要查詢的介面
0x00007fffe10228dd: test %rbx,%rbx
// 跳轉到---- L_no_such_interface ----
0x00007fffe10228e0: je 0x00007fffe1022a8c
0x00007fffe10228e6: add $0x10,%r13
0x00007fffe10228ea: mov 0x0(%r13),%rbx
0x00007fffe10228ee: cmp %rbx,%rax
// 如果還是沒有在itableOffsetEntry中找到介面類,
// 則跳轉到search繼續進行查詢
0x00007fffe10228f1: jne 0x00007fffe10228dd // 跳轉到---- search ----
// **** found_method ****
// 已經找到匹配介面的itableOffsetEntry,獲取
// itableOffsetEntry的offset屬性並儲存到%r13d中
0x00007fffe10228f3: mov 0x8(%r13),%r13d
// 通過recv_klass進行偏移後找到此介面下宣告
// 的一系列方法的開始位置
0x00007fffe10228f7: mov (%rdx,%r13,1),%rbx
我們需要重點關注itable的分派邏輯,首先生成了如下彙編:
mov 0x118(%rdx),%r13d
%rdx中儲存的是recv.Klass,獲取Klass中vtable_length屬性的值,有了這個值,我們就可以計算出vtable的大小,從而計算出itable的開始地址。
接著執行了如下彙編:
lea 0x1b8(%rdx,%r13,8),%r13
其中的0x1b8表示的是recv.Klass首地址到vtable的距離,這樣最終的%r13指向的是itable的首地址。如下圖所示。
後面我們就可以開始迴圈從itableOffsetEntry中查詢匹配的介面了, 如果找到則跳轉到found\_method,在found\_method中,要找到對應的itableOffsetEntry的offset,這個offset指明瞭介面中定義的方法的儲存位置相對於Klass的偏移量,也就是找到介面對應的第一個itableMethodEntry,因為%rbx中已經儲存了itable的索引,所以根據這個索引直接定位對應的itableMethodEntry即可,我們現在合起來看如下的2個彙編:
lea (%rdx,%rbx,8),%rdx
...
mov (%rdx,%r13,1),%rbx
當執行到如上的第2個彙編時,%r13儲存的是相對於Klass例項的偏移,而%rdx在執行第1個彙編時儲存的是Klass首地址,然後根據itable索引加上了相對於第1個itableMethodEntry的偏移,這樣就找到了對應的itableMethodEntry。
第6部分
在執行如下彙編時,各個暫存器的值如下:
rbx: Method* to call
rcx: receiver
生成的彙編程式碼如下:
0x00007fffe10228fb: test %rbx,%rbx
// 如果本來應該儲存Method*的%rbx是空,則表示沒有找到
// 這個方法,跳轉到---- no_such_method ----
0x00007fffe10228fe: je 0x00007fffe1022987
// 儲存呼叫者的棧頂指標
0x00007fffe1022904: lea 0x8(%rsp),%r13
0x00007fffe1022909: mov %r13,-0x10(%rbp)
// 跳轉到Method::from_interpreted指向的例程並執行
0x00007fffe102290d: jmpq *0x58(%rbx)
// 省略should_not_reach_here()函式生成的彙編
// **** no_such_method ****
// 當沒有找到方法時,會跳轉到這裡執行
// 彈出呼叫prepare_invoke()函式壓入的返回地址
0x00007fffe1022987: pop %rbx
// 恢復讓%r13指向bcp
0x00007fffe1022988: mov -0x38(%rbp),%r13
// 恢復讓%r14指向本地變數表
0x00007fffe102298c: mov -0x30(%rbp),%r14
// ... 省略通過call_VM()函式生成的彙編來呼叫InterpreterRuntime::throw_abstractMethodError()函式
// ... 省略呼叫should_not_reach_here()函式生成的彙編程式碼
// **** no_such_interface ****
// 當沒有找到匹配的介面時執行的彙編程式碼
0x00007fffe1022a8c: pop %rbx
0x00007fffe1022a8d: mov -0x38(%rbp),%r13
0x00007fffe1022a91: mov -0x30(%rbp),%r14
// ... 省略通過call_VM()函式生成的彙編程式碼來呼叫InterpreterRuntime::throw_IncompatibleClassChangeError()函式
// ... 省略呼叫should_not_reach_here()函式生成的彙編程式碼
對於一些異常的處理這裡就不過多介紹了,有興趣的可以看一下相關彙編程式碼的實現。
由於字數限制,《虛擬機器解釋執行Java方法(下)》將在下篇中釋出
有效能問題,找HeapDump效能社群