虛擬機(jī)解釋執(zhí)行Java方法

調(diào)用Java主類的main()方法

調(diào)用過程如下圖所示。

image.png

其中淺紅色的函數(shù)由主線程執(zhí)行,而另外的淺綠色部分由另外一個(gè)線程執(zhí)行,淺綠色的線程最終也會負(fù)責(zé)執(zhí)行Java主類中的main()方法。在JavaMain()函數(shù)中調(diào)用LoadMainClass()函數(shù)加載Java主類。接著在JavaMain()函數(shù)中有如下調(diào)用:

源代碼位置:openjdk/jdk/src/share/bin/java.c

mainID = (*env)->GetStaticMethodID(
 env, 
 mainClass, 
 "main", 
 "([Ljava/lang/String;)V");

env為JNIEnv*類型。調(diào)用JNIEnv類型中定義的GetStaticMethodID()函數(shù)獲取Java主類中main()方法的方法唯一ID,調(diào)用GetStaticMethodID()函數(shù)就是調(diào)用jni_GetStaticMethodID()函數(shù),此函數(shù)的實(shí)現(xiàn)如下:

源代碼位置: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類已經(jīng)初始化完成
 klass()->initialize(CHECK_NULL);

 Method* m;
 if ( name == vmSymbols::object_initializer_name() || 查找的是<init>方法
      name == vmSymbols::class_initializer_name() ) { 查找的是<clinit>方法
   // 因?yàn)橐檎业氖菢?gòu)造函數(shù),構(gòu)造函數(shù)沒有繼承特性,所以當(dāng)前類找不到時(shí)不向父類中繼續(xù)查找
   if (klass->oop_is_instance()) {
      // find_method()函數(shù)不會向上查找
      m = InstanceKlass::cast(klass())->find_method(name, signature); 
   } else {
      m = NULL;
   }
 } else {
   // lookup_method()函數(shù)會向上查找
   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);
}

調(diào)用的InstanceKlass::get_jmethod_id()函數(shù)獲取唯一ID,關(guān)于如何獲取或生成ID的過程這里不再詳細(xì)介紹,有興趣的自行研究。

在JavaMain()函數(shù)中有如下調(diào)用:

mainArgs = CreateApplicationArgs(env, argv, argc);
 
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

通過調(diào)用CallStaticVoidMethod()函數(shù)來調(diào)用Java主類中的main()方法??刂茩?quán)轉(zhuǎn)移到Java主類中的main()方法之中。調(diào)用CallStaticVoidMethod()函數(shù)就是調(diào)用jni_CallStaticVoidMethod()函數(shù),此函數(shù)的實(shí)現(xiàn)如下:

源代碼位置: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方法的參數(shù)以C的可變長度參數(shù)傳入后,使用JNI_ArgumentPusherVaArg實(shí)例ap是將其封裝起來。JNI_ArgumentPusherVaArg類的繼承體系如下:

JNI_ArgumentPusherVaArg->JNI_ArgumentPusher->SignatureIterator

調(diào)用的jni_invoke_static()函數(shù)的實(shí)現(xiàn)如下:

// 通過jni的方式調(diào)用Java靜態(tài)方法
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();
  // 這里進(jìn)一步將要傳給Java的參數(shù)轉(zhuǎn)換為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()函數(shù)來調(diào)用Java主類的main()方法。關(guān)于JavaCalls::call()函數(shù)大家應(yīng)該不會陌生,這個(gè)函數(shù)是怎么建立Java棧幀以及找到Java方法入口在后續(xù)執(zhí)行引擎-CallStub棧幀詳細(xì)介紹,這里不再介紹。

解釋執(zhí)行main()方法小實(shí)例

我們在介紹完一些常用字節(jié)碼指令的匯編代碼執(zhí)行邏輯后,基本看到一個(gè)main()方法從開始調(diào)用、棧幀建立、字節(jié)碼執(zhí)行的整個(gè)邏輯了,但是方法退棧、同步方法以及異常拋出等知識點(diǎn)還沒有

為Java方法創(chuàng)建的棧幀,如下圖所示。

image.png

調(diào)用完generate_fixed_frame()函數(shù)后一些寄存器中保存的值如下:

rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地變量表第1個(gè)參數(shù)的地址

現(xiàn)在我們舉一個(gè)例子,來完整的走一下解釋執(zhí)行的過程。這個(gè)例子如下:

package com.classloading;
 
public class Test {
    public static void main(String[] args) {
        int i = 0;
        i = i++;
    }
}

通過javap -verbose Test.class命令反編譯后的字節(jié)碼文件內(nèi)容如下:

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
}

如上實(shí)例對應(yīng)的棧幀狀態(tài)如下圖所示。


image.png

現(xiàn)在我們就以解釋執(zhí)行的方式執(zhí)行main()方法中的字節(jié)碼。由于是從虛擬機(jī)調(diào)用過來的,而調(diào)用完generate_fixed_frame()函數(shù)后一些寄存器中保存的值并沒有涉及到棧頂緩存,所以需要從iconst_0這個(gè)字節(jié)碼指令的vtos入口進(jìn)入,然后找到iconst_0這個(gè)字節(jié)碼指令對應(yīng)的機(jī)器指令片段。

現(xiàn)在回顧一下字節(jié)碼分派的邏輯,在generate_normal_entry()函數(shù)中會調(diào)用generate_fixed_frame()函數(shù)為Java方法的執(zhí)行生成對應(yīng)的棧幀,接下來還會調(diào)用dispatch_next()函數(shù)執(zhí)行Java方法的字節(jié)碼,首次獲取字節(jié)碼時(shí)的匯編如下:

// 在generate_fixed_frame()方法中已經(jīng)讓%r13存儲了bcp
movzbl 0x0(%r13),%ebx // %ebx中存儲的是字節(jié)碼的操作碼
  
// $0x7ffff73ba4a0這個(gè)地址指向的是對應(yīng)state狀態(tài)下的一維數(shù)組,長度為256
movabs $0x7ffff73ba4a0,%r10
  
// 注意%r10中存儲的是常量,根據(jù)計(jì)算公式%r10+%rbx*8來獲取指向存儲入口地址的地址,
// 通過*(%r10+%rbx*8)獲取到入口地址,然后跳轉(zhuǎn)到入口地址執(zhí)行
jmpq *(%r10,%rbx,8)

注意如上的$0x7ffff73ba4a0這個(gè)常量值已經(jīng)表示了棧頂緩存狀態(tài)為vtos下的一維數(shù)組首地址。而在首次進(jìn)行方法的字節(jié)碼分派時(shí),通過0x0(%r13)即可取出字節(jié)碼對應(yīng)的Opcode,使用這個(gè)Opcode可定位到iconst_0的入口地址。

%r10指向的是對應(yīng)棧頂緩存狀態(tài)state下的一維數(shù)組,長度為256,其中存儲的值為Opcode,示意圖如下圖所示。

image.png

現(xiàn)在就是看入口為vtos,出口為itos的iconst_0所要執(zhí)行的匯編代碼了,如下:

 
// vtos入口
mov $0x1,%eax
 
...
// iconst_0對應(yīng)的匯編代碼
xor    %eax,%eax

匯編指令足夠簡單,最后將值存儲到了%eax中,所以也就是棧頂緩存的出口狀態(tài)為itos。

上圖紫色的部分是本地變量表,由于本地變量表的大小為2,所以我畫了2個(gè)方格表示slot。

執(zhí)行下一個(gè)字節(jié)碼指令istore_1,也會執(zhí)行字節(jié)碼分派相關(guān)的邏輯。這里需要提醒下,其實(shí)之前在介紹字節(jié)碼指令對應(yīng)的匯編時(shí),只關(guān)注了字節(jié)碼指令本身的執(zhí)行邏輯,其實(shí)在為每個(gè)字節(jié)碼指令生成機(jī)器指令時(shí),一般都會為這些字節(jié)碼指令生成3部分機(jī)器指令片段:

(1)不同棧頂狀態(tài)對應(yīng)的入口執(zhí)行邏輯;

(2)字節(jié)碼指令本身需要執(zhí)行的邏輯;

(3)分派到下一個(gè)字節(jié)碼指令的邏輯。

對于字節(jié)碼指令模板定義中,如果flags中指令有disp,那么這些指令自己會含有分派的邏輯,如goto、ireturn、tableswitch、lookupswitch、jsr等。由于我們的指令是iconst_0,所以會為這個(gè)字節(jié)碼指令生成分派邏輯,生成的邏輯如下:

movzbl 0x1(%r13),%ebx    // %ebx中存儲的是字節(jié)碼的操作碼
  
movabs itos對應(yīng)的一維數(shù)組的首地址,%r10
 
jmpq *(%r10,%rbx,8)

需要注意的是,如果要讓%ebx中存儲istore_1的Opcode,則%r13需要加上iconst_0指令的長度,即1。由于iconst_0執(zhí)行后的出口棧頂緩存為itos,所以要找到入口狀態(tài)為itos,而Opcode為istore_1的機(jī)器指令片段執(zhí)行。指令片段如下:

mov    %eax,-0x8(%r14)

代碼將棧頂?shù)闹?eax存儲到本地變量表下標(biāo)索引為1的位置處。通過%r14很容易定位到本地變量表的位置,執(zhí)行完成后的棧狀態(tài)如下圖所示。

image.png

執(zhí)行iconst_0和istore_1時(shí),整個(gè)過程沒有向表達(dá)式棧(上圖中sp/rsp開始以下的部分就是表達(dá)式棧)中壓入0,實(shí)際上如果沒有棧頂緩存的優(yōu)化,應(yīng)該將0壓入棧頂,然后彈出棧頂存儲到局部變量表,但是有了棧頂緩存后,沒有壓棧操作,也就有彈棧操作,所以能極大的提高程序的執(zhí)行效率。

return指令判斷的邏輯比較多,主要是因?yàn)橛行┓椒赡苡衧ynchronized關(guān)鍵字,所以會在方法棧中保存鎖相關(guān)的信息,而在return返回時(shí),退棧要釋放鎖。不過我們現(xiàn)在只看針對本實(shí)例要運(yùn)行的部分代碼,如下:

// 將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
// 如果方法不是同步方法,跳轉(zhuǎn)到----unlocked----
0x00007fffe101b78c: je 0x00007fffe101b970

main()方法為非同步方法,所以跳轉(zhuǎn)到unlocked處執(zhí)行,在unlocked處執(zhí)行的邏輯中會執(zhí)行一些釋放鎖的邏輯,對于我們本實(shí)例來說這不重要,我們直接看退棧的操作,如下:

// 將-0x8(%rbp)處保存的old stack pointer(saved rsp)取出來放到%rbx中
0x00007fffe101bac7: mov -0x8(%rbp),%rbx
 
// 移除棧幀
// leave指令相當(dāng)于:
// mov %rbp, %rsp
// pop %rbp
0x00007fffe101bacb: leaveq 
// 將返回地址彈出到%r13中
0x00007fffe101bacc: pop %r13
// 設(shè)置%rsp為調(diào)用者的棧頂值
0x00007fffe101bace: mov %rbx,%rsp
0x00007fffe101bad1: jmpq *%r13

這個(gè)匯編不難,這里不再繼續(xù)介紹。退棧后的棧狀態(tài)如下圖所示。


image.png

這就完全回到了調(diào)用Java方法之前的棧狀態(tài),接下來如何退出如上棧幀并結(jié)束方法調(diào)用就是C++語言的事兒了。

方法調(diào)用指令之invokevirtual

invokevirtual字節(jié)碼指令的模板定義如下:

def(Bytecodes::_invokevirtual , ubcp|disp|clvm|____, vtos, vtos, invokevirtual , f2_byte );

生成函數(shù)為invokevirtual,傳遞的參數(shù)為f2_byte,也就是2,如果為2時(shí),ConstantPoolCacheEntry::indices中取[b2,b1,original constant pool index]中的b2部分。調(diào)用的TemplateTable::invokevirtual()函數(shù)的實(shí)現(xiàn)如下:

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);
}

先調(diào)用prepare_invoke()函數(shù),后調(diào)用invokevirtual_helper()函數(shù)來生成invokevirtual字節(jié)碼指令對應(yīng)的匯編代碼(其實(shí)是生成機(jī)器指令,然后反編譯對應(yīng)的匯編代碼,在后面我們就直接表述為匯編代碼,讀者要知道)。

1、prepare_invoke()函數(shù)

調(diào)用TemplateTable::prepare_invoke()函數(shù)生成的匯編代碼比較多,所以我們分三部分進(jìn)行查看。

第1部分:

0x00007fffe1021f90: mov %r13,-0x38(%rbp) // 將bcp保存到棧中
// invokevirtual x中取出x,也就是常量池索引存儲到%edx,
// 其實(shí)這里已經(jīng)是ConstantPoolCacheEntry的index,因?yàn)樵陬惖倪B接
// 階段會對方法中特定的一些字節(jié)碼指令進(jìn)行重寫
0x00007fffe1021f94: movzwl 0x1(%r13),%edx 
// 將ConstantPoolCache的首地址存儲到%rcx


0x00007fffe1021f99: mov -0x28(%rbp),%rcx 

// 左移2位,因?yàn)?edx中存儲的是ConstantPoolCacheEntry索引,左移2位是因?yàn)?// ConstantPoolCacheEntry占用4個(gè)字
0x00007fffe1021f9d: shl $0x2,%edx 
       
// 計(jì)算%rcx+%rdx*8+0x10,獲取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices
// 因?yàn)镃onstantPoolCache的大小為0x16字節(jié),%rcx+0x10定位
// 到第一個(gè)ConstantPoolCacheEntry的位置
// %rdx*8算出來的是相對于第一個(gè)ConstantPoolCacheEntry的字節(jié)偏移
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是否已經(jīng)連接 
0x00007fffe1021fad: cmp $0xb6,%ebx 
 
// 如果連接就進(jìn)行跳轉(zhuǎn),跳轉(zhuǎn)到resolved 
0x00007fffe1021fb3: je 0x00007fffe1022052

主要查看字節(jié)碼是否已經(jīng)連接,如果沒有連接則需要連接,如果已經(jīng)進(jìn)行了連接,則跳轉(zhuǎn)到resolved直接執(zhí)行方法調(diào)用操作。

第2部分:

// 調(diào)用InterpreterRuntime::resolve_invoke()函數(shù),因?yàn)橹噶钸€沒有連接
// 將bytecode為182的指令移動到%ebx中
0x00007fffe1021fb9: mov $0xb6,%ebx 

// 通過調(diào)用MacroAssembler::call_VM()函數(shù)來調(diào)用
// InterpreterRuntime::resolve_invoke(JavaThread* thread, Bytecodes::Code bytecode)函數(shù)
// 進(jìn)行方法連接
0x00007fffe1021fbe: callq 0x00007fffe1021fc8 
0x00007fffe1021fc3: jmpq 0x00007fffe1022046 // 跳轉(zhuǎn)到----E----
// 準(zhǔn)備第2個(gè)參數(shù),也就是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 
// 結(jié)束MacroAssembler::call_VM()函數(shù)的調(diào)用

// **** E ****
// 將invokevirtual x中的x加載到%edx中,也就是ConstantPoolCacheEntry的索引
0x00007fffe1022046: movzwl 0x1(%r13),%edx 

// 將ConstantPoolCache的首地址存儲到%rcx中 
0x00007fffe102204b: mov -0x28(%rbp),%rcx 

 // %edx中存儲的是ConstantPoolCacheEntry index,轉(zhuǎn)換為字偏移
0x00007fffe102204f: shl $0x2,%edx

方法連接的邏輯和之前介紹的字段的連接邏輯類似,都是完善ConstantPoolCache中對應(yīng)的ConstantPoolCacheEntry添加相關(guān)信息。

調(diào)用InterpreterRuntime::resolve_invoke()函數(shù)進(jìn)行方法連接,這個(gè)函數(shù)的實(shí)現(xiàn)比較多,我們在下一篇中詳細(xì)介紹。連接完成后ConstantPoolCacheEntry中的各個(gè)項(xiàng)如下圖所示。

image.png

所以對于invokevirtual來說,通過vtable進(jìn)行方法的分發(fā),在ConstantPoolCacheEntry中,_f1字段沒有使用,而對_f2字段來說,如果調(diào)用的是非final的virtual方法,則保存的是目標(biāo)方法在vtable中的索引編號,如果是virtual final方法,則_f2字段直接指向目標(biāo)方法的Method實(shí)例。

第3部分:

// **** resolved ****

// resolved的定義點(diǎn),到這里說明invokevirtual字節(jié)碼已經(jīng)連接
// 獲取ConstantPoolCacheEntry::_f2,這個(gè)字段只對virtual有意義
// 在計(jì)算時(shí),因?yàn)镃onstantPoolCacheEntry在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中取出參數(shù)大小 
0x00007fffe102205d: and $0xff,%ecx 

         
// 獲取到recv,%rcx中保存的是參數(shù)大小,最終計(jì)算參數(shù)所需要的大小為%rsp+%rcx*8-0x8,
// flags中的參數(shù)大小對實(shí)例方法來說,已經(jīng)包括了recv的大小
// 如調(diào)用實(shí)例方法的第一個(gè)參數(shù)是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,計(jì)算返回地址
// 因?yàn)門emplateInterpreter::invoke_return_entry是數(shù)組,
// 所以要找到對應(yīng)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保存了一段例程的入口,這段例程在后面會詳細(xì)介紹。

執(zhí)行完如上的代碼后,已經(jīng)向相關(guān)的寄存器中存儲了相關(guān)的值。相關(guān)的寄存器狀態(tài)如下:

rbx: 存儲的是ConstantPoolCacheEntry::_f2屬性的值
rcx: 就是調(diào)用實(shí)例方法時(shí)的第一個(gè)參數(shù)this
rdx: 存儲的是ConstantPoolCacheEntry::_flags屬性的值

棧的狀態(tài)如下圖所示。

棧中壓入了TemplateInterpreter::invoke_return_entry的返回地址。

2、invokevirtual_helper()函數(shù)

調(diào)用TemplateTable::invokevirtual_helper()函數(shù)生成的代碼如下:

// flags存儲到%eax
0x00007fffe1022084: mov %edx,%eax 
// 測試調(diào)用的方法是否為final 
0x00007fffe1022086: and $0x100000,%eax 
// 如果不為final就直接跳轉(zhuǎn)到----notFinal---- 
0x00007fffe102208c: je 0x00007fffe10220c0 

// 通過(%rcx)來獲取receiver的值,如果%rcx為空,則會引起OS異常
0x00007fffe1022092: cmp (%rcx),%rax 

// 省略統(tǒng)計(jì)相關(guān)代碼部分

// 設(shè)置調(diào)用者棧頂并保存
0x00007fffe10220b4: lea 0x8(%rsp),%r13
0x00007fffe10220b9: mov %r13,-0x10(%rbp)

// 跳轉(zhuǎn)到Method::_from_interpretered_entry入口去執(zhí)行
0x00007fffe10220bd: jmpq *0x58(%rbx)

對于final方法來說,其實(shí)沒有動態(tài)分派,所以也不需要通過vtable進(jìn)行目標(biāo)查找。調(diào)用時(shí)的棧如下圖所示。

image.png

如下代碼是通過vtable查找動態(tài)分派需要調(diào)用的方法入口 。

// **** notFinal ****

// invokevirtual指令調(diào)用的如果是非final方法,直接跳轉(zhuǎn)到這里
// %rcx中存儲的是receiver,用oop來表示。通過oop獲取Klass
0x00007fffe10220c0: mov 0x8(%rcx),%eax 

// 調(diào)用MacroAssembler::decode_klass__not_null()函數(shù)生成下面的一個(gè)匯編代碼
0x00007fffe10220c3: shl $0x3,%rax // LogKlassAlignmentInBytes=0x03

// 省略統(tǒng)計(jì)相關(guān)代碼部分


// %rax中存儲的是recv_klass
// %rbx中存儲的是vtable_index,
// 而0x1b8為InstanceKlass::vtable_start_offset()*wordSize+vtableEntry::method_offset_in_bytes(),
// 其實(shí)就是通過動態(tài)分派找到需要調(diào)用的Method*并存儲到%rbx中
0x00007fffe1022169: mov 0x1b8(%rax,%rbx,8),%rbx

// 設(shè)置調(diào)用者的棧頂?shù)刂凡⒈4?0x00007fffe1022171: lea 0x8(%rsp),%r13
0x00007fffe1022176: mov %r13,-0x10(%rbp)

// 跳轉(zhuǎn)到Method::_from_interpreted_entry處執(zhí)行
0x00007fffe102217a: jmpq *0x58(%rbx)

理解如上代碼時(shí)需要知道vtable方法分派以及vtable在InstanceKlass中的布局,這在《深入剖析Java虛擬機(jī):源碼剖析與實(shí)例詳解》一書中詳細(xì)介紹過,這里不再介紹。

跳轉(zhuǎn)到Method::_from_interpretered_entry保存的例程處執(zhí)行,也就是以解釋執(zhí)行運(yùn)行invokevirtual字節(jié)碼指令調(diào)用的目標(biāo)方法,關(guān)于Method::_from_interpretered_entry保存的例程的邏輯在第6篇、第7篇、第8篇中詳細(xì)介紹過,這里不再介紹。

如上的匯編語句 mov 0x1b8(%rax,%rbx,8),%rbx 是通過調(diào)用調(diào)用lookup_virtual_method()函數(shù)生成的,此函數(shù)將vtable_entry_addr加載到%rbx中,實(shí)現(xiàn)如下:

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屬性的值。

最后還要說一下,如上生成的一些匯編代碼中省略了統(tǒng)計(jì)相關(guān)的執(zhí)行邏輯,這里統(tǒng)計(jì)相關(guān)的代碼也是非常重要的,它會輔助進(jìn)行編譯,所以后面我們還會介紹這些統(tǒng)計(jì)相關(guān)的邏輯。

第32篇-解析interfacevirtual字節(jié)碼指令
在前面介紹invokevirtual指令時(shí),如果判斷出ConstantPoolCacheEntry中的_indices字段的_f2屬性的值為空,則認(rèn)為調(diào)用的目標(biāo)方法沒有連接,也就是沒有向ConstantPoolCacheEntry中保存調(diào)用方法的相關(guān)信息,需要調(diào)用InterpreterRuntime::resolve_invoke()函數(shù)進(jìn)行方法連接,這個(gè)函數(shù)的實(shí)現(xiàn)比較多,我們分幾部分查看:

InterpreterRuntime::resolve_invoke()函數(shù)第1部分:

Handle receiver(thread, NULL);
if (bytecode == Bytecodes::_invokevirtual || bytecode == Bytecodes::_invokeinterface) {
    ResourceMark rm(thread);
    // 調(diào)用method()函數(shù)從當(dāng)前的棧幀中獲取到需要執(zhí)行的方法
    Method* m1 = method(thread);
    methodHandle m (thread, m1);
 
    // 調(diào)用bci()函數(shù)從當(dāng)前的棧幀中獲取需要執(zhí)行的方法的字節(jié)碼索引
    int i1 = bci(thread);
    Bytecode_invoke call(m, i1);
 
    // 當(dāng)前需要執(zhí)行的方法的簽名
    Symbol* signature = call.signature();
 
    frame fm = thread->last_frame();
    oop x = fm.interpreter_callee_receiver(signature);
    receiver = Handle(thread,x);
}

當(dāng)字節(jié)碼為invokevirtual或invokeinterface這樣的動態(tài)分派字節(jié)碼時(shí),執(zhí)行如上的邏輯。獲取到了receiver變量的值。接著看實(shí)現(xiàn),如下:

InterpreterRuntime::resolve_invoke()函數(shù)第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);
    ...
} 
 
// 如果已經(jīng)向ConstantPoolCacheEntry中更新了調(diào)用的相關(guān)信息則直接返回
if (already_resolved(thread))
  return;

根據(jù)存儲在當(dāng)前棧中的bcp來獲取字節(jié)碼指令的操作數(shù),這個(gè)操作數(shù)通常就是常量池緩存項(xiàng)索引。然后調(diào)用LinkResolver::resolve_invoke()函數(shù)進(jìn)行方法連接。 這個(gè)函數(shù)會間接調(diào)用LinkResolver::resolve_invokevirtual()函數(shù),實(shí)現(xiàn)如下:

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);
}

其中會調(diào)用resolve_pool()和resolve_vritual_call()函數(shù)分別連接常量池和方法調(diào)用指令。調(diào)用會涉及到的相關(guān)函數(shù)大概如下圖所示。

image.png

下面介紹resolve_pool()和resolve_virtual_call()函數(shù)及其調(diào)用的相關(guān)函數(shù)的實(shí)現(xiàn)。

01 resolve_pool()函數(shù)

調(diào)用的resolve_pool()函數(shù)會調(diào)用一些函數(shù),如下圖所示。

image.png

每次調(diào)用LinkResolver::resolve_pool()函數(shù)時(shí)不一定會按如上的函數(shù)調(diào)用鏈執(zhí)行,但是當(dāng)類還沒有解析時(shí),通常會調(diào)用SystemDictionary::resolve_or_fail()函數(shù)進(jìn)行解析,最終會獲取到指向Klass實(shí)例的指針,最終將這個(gè)類更新到常量池中。

resolve_pool()函數(shù)的實(shí)現(xiàn)如下:

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為常量池緩存項(xiàng)的索引。resolved_klass參數(shù)表示需要進(jìn)行解析的類(解析是在類生成周期中連接相關(guān)的部分,所以我們之前有時(shí)候會稱為連接,其實(shí)具體來說是解析的意思),而current_klass為當(dāng)前擁有常量池的類,由于傳遞參數(shù)時(shí)是C++的引用傳遞,所以同值會直接改變變量的值,調(diào)用者中的值也會隨著改變。

調(diào)用resolve_klass()函數(shù)進(jìn)行類解析,一般來說,類解析會在解釋常量池項(xiàng)時(shí)就會進(jìn)行,這在《深入剖析Java虛擬機(jī):源碼剖析與實(shí)例詳解(基礎(chǔ)卷)》一書中介紹過,這里需要再說一下。

調(diào)用的resolve_klass()函數(shù)及相關(guān)函數(shù)的實(shí)現(xiàn)如下:

void LinkResolver::resolve_klass(
 KlassHandle& result,
 constantPoolHandle pool,
 int                  index,
 TRAPS
) {
  Klass* result_oop = pool->klass_ref_at(index, CHECK);
  // 通過引用進(jìn)行傳遞
  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);
}

調(diào)用的impl_klass_ref_index_at()函數(shù)的實(shí)現(xiàn)如下:

int ConstantPool::impl_klass_ref_index_at(int which, bool uncached) {
  int i = which;
  if (!uncached && cache() != NULL) {
    // 從which對應(yīng)的ConstantPoolCacheEntry項(xiàng)中獲取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);
}

根據(jù)斷言可知,在原常量池索引的i處的項(xiàng)肯定為JVM_CONSTANT_Fieldref、JVM_CONSTANT_Methodref或JVM_CONSTANT_InterfaceMethodref,這幾項(xiàng)的格式如下:

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項(xiàng)的格式都一樣,其中的class_index索引處的項(xiàng)必須為CONSTANT_Class_info結(jié)構(gòu),表示一個(gè)類或接口,當(dāng)前類字段或方法是這個(gè)類或接口的成員。name_and_type_index索引處必須為CONSTANT_NameAndType_info項(xiàng)。

通過調(diào)用int_at_addr()函數(shù)和extract_low_short_from_int()函數(shù)獲取class_index的索引值,如果了解了常量池內(nèi)存布局,這里函數(shù)的實(shí)現(xiàn)理解起來會很簡單,這里不再介紹。

在klass_ref_at()函數(shù)中調(diào)用klass_at()函數(shù),此函數(shù)的實(shí)現(xiàn)如下:

Klass* klass_at(int which, TRAPS) {
    constantPoolHandle h_this(THREAD, this);
    return klass_at_impl(h_this, which, CHECK_NULL);
}

調(diào)用的klass_at_impl()函數(shù)的實(shí)現(xiàn)如下:

Klass* ConstantPool::klass_at_impl(
 constantPoolHandle this_oop,
 int which,
 TRAPS
) {
   
  CPSlot entry = this_oop->slot_at(which);
  if (entry.is_resolved()) { // 已經(jīng)進(jìn)行了連接
    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
 
  // 省略當(dāng)in_error變量的值為true時(shí)的處理邏輯
  
  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()); // 注意這里會更新常量池中存儲的內(nèi)容,這樣就表示類已經(jīng)解析完成,下次就不需要重復(fù)解析了
      }
    }
  }
 
  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();
}

函數(shù)首先調(diào)用slot_at()函數(shù)獲取常量池中一個(gè)slot中存儲的值,然后通過CPSlot來表示這個(gè)slot,這個(gè)slot中可能存儲的值有2個(gè),分別為指向Symbol實(shí)例(因?yàn)轭惷肅ONSTANT_Utf8_info項(xiàng)表示,在虛擬機(jī)內(nèi)部統(tǒng)一使用Symbol對象表示字符串)的指針和指向Klass實(shí)例的指針,如果類已經(jīng)解釋,那么指針表示的地址的最后一位為0,如果還沒有被解析,那么地址的最后一位為1。

當(dāng)沒有解析時(shí),需要調(diào)用SystemDictionary::resolve_or_fail()函數(shù)獲取類Klass的實(shí)例,然后更新常量池中的信息,這樣下次就不用重復(fù)解析類了。最后返回指向Klass實(shí)例的指針即可。

繼續(xù)回到LinkResolver::resolve_pool()函數(shù)看接下來的執(zhí)行邏輯,也就是會獲取JVM_CONSTANT_Fieldref、JVM_CONSTANT_Methodref或JVM_CONSTANT_InterfaceMethodref項(xiàng)中的name_and_type_index,其指向的是CONSTANT_NameAndType_info項(xiàng),格式如下:

CONSTANT_NameAndType_info{
   u1 tag;
  u2 name_index;
  u2 descriptor index;
}

獲取邏輯就是先根據(jù)常量池緩存項(xiàng)的索引找到原常量池項(xiàng)的索引,然后查找到CONSTANT_NameAndType_info后,獲取到方法名稱和簽名的索引,進(jìn)而獲取到被調(diào)用的目標(biāo)方法的名稱和簽名。這些信息將在接下來調(diào)用的resolve_virtual_call()函數(shù)中使用。

02 resolve_virtual_call()函數(shù)

resolve_virtual_call()函數(shù)會調(diào)用的相關(guān)函數(shù)如下圖所示。

image.png

LinkResolver::resolve_virtual_call()的實(shí)現(xiàn)如下:

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);
}

首先調(diào)用LinkResolver::linktime_resolve_virtual_method()函數(shù),這個(gè)函數(shù)會調(diào)用如下函數(shù):

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()) { 
    // 從解析類實(shí)現(xiàn)的所有接口(包括間接實(shí)現(xiàn)的接口)中查找方法
    lookup_method_in_interfaces(resolved_method, resolved_klass, method_name, method_signature, CHECK);
    // ...
 
    if (resolved_method.is_null()) {
      // 沒有找到對應(yīng)的方法
      ...
    }
  }
 
  // ...
}

如上函數(shù)中最主要的就是根據(jù)method_name和method_signature從resolved_klass類中找到合適的方法,如果找到就賦值給resolved_method變量。

調(diào)用lookup_method_in_klasses()、lookup_method_in_interfaces()等函數(shù)進(jìn)行方法的查找,這里暫時(shí)不介紹。

下面接著看runtime_resolve_virtual_method()函數(shù),這個(gè)函數(shù)的實(shí)現(xiàn)如下:

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;
 
  // 當(dāng)方法定義在接口中時(shí),表示是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方法,需要動態(tài)分派且肯定有正確的vtable索引
    vtable_index = resolved_method->vtable_index();
 
    // 有些方法雖然看起來需要動態(tài)分派,但是如果這個(gè)方法有final關(guān)鍵字時(shí),可進(jìn)行靜態(tài)綁定,所以直接調(diào)用即可
    // final方法其實(shí)不會放到vtable中,除非final方法覆寫了父類中的方法
    if (vtable_index == Method::nonvirtual_vtable_index) {
      selected_method = resolved_method;
    } else {
      // 根據(jù)vtable和vtable_index以及inst進(jìn)行方法的動態(tài)分派
      InstanceKlass* inst = (InstanceKlass*)recv_klass();
      selected_method = methodHandle(THREAD, inst->method_at_vtable(vtable_index));
    }
  } 
  
  // setup result resolve的類型為CallInfo,為CallInfo設(shè)置了連接后的相關(guān)信息
  result.set_virtual(resolved_klass, recv_klass, resolved_method, selected_method, vtable_index, CHECK);
}

當(dāng)為miranda方法時(shí),調(diào)用 LinkResolver::vtable_index_of_interface_method()函數(shù)查找;當(dāng)為final方法時(shí),因?yàn)閒inal方法不可能被子類覆寫,所以resolved_method就是目標(biāo)調(diào)用方法;除去前面的2種情況后,剩下的方法就需要結(jié)合vtable和vtable_index進(jìn)行動態(tài)分派了。

如上函數(shù)將查找到調(diào)用時(shí)需要的所有信息并存儲到CallInfo類型的result變量中。

在獲取到調(diào)用時(shí)的所有信息并存儲到CallInfo中后,就可以根據(jù)info中相關(guān)信息填充ConstantPoolCacheEntry。我們回看InterpreterRuntime::resolve_invoke()函數(shù)的執(zhí)行邏輯。

InterpreterRuntime::resolve_invoke()函數(shù)第2部分:

switch (info.call_kind()) {
  case CallInfo::direct_call: // 直接調(diào)用
    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();
}

無論直接調(diào)用,還是vtable和itable動態(tài)分派,都會在方法解析完成后將相關(guān)的信息存儲到常量池緩存項(xiàng)中。調(diào)用cache_entry()函數(shù)獲取對應(yīng)的ConstantPoolCacheEntry項(xiàng),然后調(diào)用set_vtable_call()函數(shù),此函數(shù)會調(diào)用如下函數(shù)更新ConstantPoolCacheEntry項(xiàng)中的信息,如下:

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指令時(shí),并不一定都是動態(tài)分發(fā),也有可能是靜態(tài)綁定
    case Bytecodes::_invokevirtual: // 當(dāng)前已經(jīng)在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), // 在接口中調(diào)用Object中定義的方法
                           method()->size_of_parameters());
          set_f2_as_vfinal_method(method());
        } else {
          // 執(zhí)行這里的邏輯時(shí),表示方法是非靜態(tài)綁定的非final方法,需要動態(tài)分派,則vtable_index的值肯定大于等于0
          set_method_flags(as_TosState(method->result_type()),
                           ((change_to_virtual ? 1 : 0) << is_forced_virtual_shift),
                           method()->size_of_parameters());
          // 對于動態(tài)分發(fā)來說,ConstantPoolCacheEntry::_f2中保存的是vtable_index
          set_f2(vtable_index);
        }
        byte_no = 2;
        break;
      }
      // ...
  }
 
  if (byte_no == 1) {
    // invoke_code為非invokevirtual和非invokeinterface字節(jié)碼指令
    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中的各個(gè)項(xiàng)如下圖所示。


image.png

所以對于invokevirtual來說,通過vtable進(jìn)行方法的分發(fā),在ConstantPoolCacheEntry中,_f1字段沒有使用,而對_f2字段來說,如果調(diào)用的是非final的virtual方法,則保存的是目標(biāo)方法在vtable中的索引編號,如果是virtual final方法,則_f2字段直接指向目標(biāo)方法的Method實(shí)例。

方法調(diào)用指令之invokeinterface

invokevirtual字節(jié)碼指令的模板定義如下:

def(Bytecodes::_invokeinterface , ubcp|disp|clvm|____, vtos, vtos, invokeinterface , f1_byte );

可以看到指令的生成函數(shù)為TemplateTable::invokeinterface(),在這個(gè)函數(shù)中首先會調(diào)用TemplateTable::prepare_invoke()函數(shù),TemplateTable::prepare_invoke()函數(shù)生成的匯編代碼如下:

第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
// 如果已經(jīng)連接,那這個(gè)b1應(yīng)該等于185,也就是invokeinterface指令的操作碼
0x00007fffe1022624: shr $0x10,%ebx
0x00007fffe1022627: and $0xff,%ebx
0x00007fffe102262d: cmp $0xb9,%ebx
// 如果invokeinterface已經(jīng)連接就跳轉(zhuǎn)到----resolved----
0x00007fffe1022633: je 0x00007fffe10226d2

匯編代碼的判斷邏輯與invokevirutal一致,這里不在過多解釋。

第2部分

由于方法還沒有解析,所以需要設(shè)置ConstantPoolCacheEntry中的信息,這樣再一次調(diào)用時(shí)就不需要重新找調(diào)用相關(guān)的信息了。生成的匯編如下:

// 執(zhí)行如下匯編代碼時(shí),表示invokeinterface指令還沒有連接,也就是ConstantPoolCacheEntry中
// 還沒有保存調(diào)用相關(guān)的信息
   
// 通過調(diào)用call_VM()函數(shù)生成如下匯編,通過這些匯編
// 調(diào)用InterpreterRuntime::resolve_invoke()函數(shù)
// 將bytecode存儲到%ebx中
0x00007fffe1022639: mov $0xb9,%ebx 
// 通過MacroAssembler::call_VM()來調(diào)用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 
 
// 結(jié)束MacroAssembler::call_VM()函數(shù)
// 將invokeinterface x中的x加載到%edx中
0x00007fffe10226c6: movzwl 0x1(%r13),%edx
// 將ConstantPoolCache的首地址存儲到%rcx中
0x00007fffe10226cb: mov -0x28(%rbp),%rcx
// %edx中存儲的是ConstantPoolCacheEntry項(xiàng)的索引,轉(zhuǎn)換為字節(jié)
// 偏移,因?yàn)橐粋€(gè)ConstantPoolCacheEntry項(xiàng)占用4個(gè)字
0x00007fffe10226cf: shl $0x2,%edx

與invokevirtual的實(shí)現(xiàn)類似,這里仍然在方法沒有解釋時(shí)調(diào)用InterpreterRuntime::resolve_invoke()函數(shù)進(jìn)行方法解析,后面我們也詳細(xì)介紹一下InterpreterRuntime::resolve_invoke()函數(shù)的實(shí)現(xiàn)。

在調(diào)用完resolve_invoke()函數(shù)后,會將調(diào)用相信的信息存儲到CallInfo實(shí)例info中。所以在調(diào)用的InterpreterRuntime::resolve_invoke()函數(shù)的最后會有如下的實(shí)現(xiàn):

switch (info.call_kind()) {
  case CallInfo::direct_call: // 直接調(diào)用
    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();
}

之前已經(jīng)介紹過vtable分派,現(xiàn)在看一下itable分派。

當(dāng)為itable分派時(shí),會調(diào)用set_itable_call()函數(shù)設(shè)置ConstantPoolCacheEntry中的相關(guān)信息,這個(gè)函數(shù)的實(shí)現(xiàn)如下:

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字段上,這樣當(dāng)這個(gè)字段有值時(shí)表示已經(jīng)對此方法完成了解析;

_f1字段存儲聲明方法的接口類,也就是_f1是指向表示接口的Klass實(shí)例的指針;

_f2表示_f1接口類對應(yīng)的方法表中的索引,如果是final方法,則存儲指向Method實(shí)例的指針。

解析完成后ConstantPoolCacheEntry中的各個(gè)項(xiàng)如下圖所示。

image.png

第3部分

如果invokeinterface字節(jié)碼指令已經(jīng)解析,則直接跳轉(zhuǎn)到resolved執(zhí)行,否則調(diào)用resolve_invoke進(jìn)行解析,解析完成后也會接著執(zhí)行resolved處的邏輯,如下:

// **** resolved ****
// resolved的定義點(diǎn),到這里說明invokeinterface字節(jié)碼已經(jīng)連接
 
 
// 執(zhí)行完如上匯編后寄存器的值如下:
// %edx:ConstantPoolCacheEntry index
// %rcx:ConstantPoolCache
 
// 獲取到ConstantPoolCacheEntry::_f1
// 在計(jì)算時(shí),因?yàn)镃onstantPoolCacheEntry在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
 
 
// 執(zhí)行如上匯編后寄存器的值如下:
// %rax:ConstantPoolCacheEntry::_f1
// %rbx:ConstantPoolCacheEntry::_f2
// %edx:ConstantPoolCacheEntry::_flags
 
// 將flags移動到ecx中
0x00007fffe10226e0: mov %edx,%ecx
// 從ConstantPoolCacheEntry::_flags中獲取參數(shù)大小
0x00007fffe10226e2: and $0xff,%ecx 
// 讓%rcx指向recv 
0x00007fffe10226e8: mov -0x8(%rsp,%rcx,8),%rcx 
// 暫時(shí)用%r13d保存ConstantPoolCacheEntry::_flags屬性
0x00007fffe10226ed: mov %edx,%r13d 
// 從_flags的高4位保存的TosState中獲取方法返回類型 
0x00007fffe10226f0: shr $0x1c,%edx
// 將TemplateInterpreter::invoke_return_entry地址存儲到%r10
0x00007fffe10226f3: movabs $0x7ffff73b63e0,%r10
// %rdx保存的是方法返回類型,計(jì)算返回地址
// 因?yàn)門emplateInterpreter::invoke_return_entry是數(shù)組,
// 所以要找到對應(yīng)return type的入口地址
0x00007fffe10226fd: mov (%r10,%rdx,8),%rdx
// 獲取結(jié)果處理函數(shù)TemplateInterpreter::invoke_return_entry的地址并壓入棧中
0x00007fffe1022701: push %rdx 
 
// 恢復(fù)ConstantPoolCacheEntry::_flags中%edx
0x00007fffe1022702: mov %r13d,%edx 
// 還原bcp 
0x00007fffe1022705: mov -0x38(%rbp),%r13

在TemplateTable::invokeinterface()函數(shù)中首先會調(diào)用prepare_invoke()函數(shù),上面的匯編就是由這個(gè)函數(shù)生成的。調(diào)用完后各個(gè)寄存器的值如下:

rax: interface klass (from f1)
rbx: itable index (from f2)
rcx: receiver
rdx: flags

然后接著執(zhí)行TemplateTable::invokeinterface()函數(shù)生成的匯編片段,如下:

第4部分

// 將ConstantPoolCacheEntry::_flags的值存儲到%r14d中
0x00007fffe1022709: mov %edx,%r14d
// 檢測一下_flags中是否含有is_forced_virtual_shift標(biāo)識,如果有,
// 表示調(diào)用的是Object類中的方法,需要通過vtable進(jìn)行動態(tài)分派
0x00007fffe102270c: and $0x800000,%r14d
0x00007fffe1022713: je 0x00007fffe1022812 // 跳轉(zhuǎn)到----notMethod----

// ConstantPoolCacheEntry::_flags存儲到%eax
0x00007fffe1022719: mov %edx,%eax
// 測試調(diào)用的方法是否為final
0x00007fffe102271b: and $0x100000,%eax
0x00007fffe1022721: je 0x00007fffe1022755 // 如果為非final方法,則跳轉(zhuǎn)到----notFinal----


// 下面匯編代碼是對final方法的處理

// 對于final方法來說,rbx中存儲的是Method*,也就是ConstantPoolCacheEntry::_f2指向Method*
// 跳轉(zhuǎn)到Method::from_interpreted處執(zhí)行即可
0x00007fffe1022727: cmp (%rcx),%rax
// ... 省略統(tǒng)計(jì)相關(guān)的代碼
// 設(shè)置調(diào)用者棧頂并存儲
0x00007fffe102274e: mov %r13,-0x10(%rbp)
// 跳轉(zhuǎn)到Method::_from_interpreted_entry
0x00007fffe1022752: jmpq *0x58(%rbx) // 調(diào)用final方法
 
// **** notFinal ****

// 調(diào)用load_klass()函數(shù)生成如下2句匯編
// 查看recv這個(gè)oop對應(yīng)的Klass,存儲到%eax中
0x00007fffe1022755: mov 0x8(%rcx),%eax 
// 調(diào)用decode_klass_not_null()函數(shù)生成的匯編 
0x00007fffe1022758: shl $0x3,%rax 

      
// 省略統(tǒng)計(jì)相關(guān)的代碼

// 調(diào)用lookup_virtual_method()函數(shù)生成如下這一句匯編
0x00007fffe10227fe: mov 0x1b8(%rax,%rbx,8),%rbx

// 設(shè)置調(diào)用者棧頂并存儲
0x00007fffe1022806: lea 0x8(%rsp),%r13
0x00007fffe102280b: mov %r13,-0x10(%rbp)

// 跳轉(zhuǎn)到Method::_from_interpreted_entry
0x00007fffe102280f: jmpq *0x58(%rbx)

如上匯編包含了對final和非final方法的分派邏輯。對于final方法來說,由于ConstantPoolCacheEntry::_f2中存儲的就是指向被調(diào)用的Method實(shí)例,所以非常簡單;對于非final方法來說,需要通過vtable實(shí)現(xiàn)動態(tài)分派。分派的關(guān)鍵一個(gè)匯編語句如下:

mov    0x1b8(%rax,%rbx,8),%rbx

需要提示的是,只有少量的方法可能才會走這個(gè)邏輯進(jìn)行vtable的動態(tài)分派,如調(diào)用Object類中的方法。

如果跳轉(zhuǎn)到notMethod后,那就需要通過itable進(jìn)行方法的動態(tài)分派了,我們看一下這部分的實(shí)現(xiàn)邏輯:

第5部分

// **** notMethod ****
 
// 讓%r14指向本地變量表
0x00007fffe1022812: mov -0x30(%rbp),%r14 
// %rcx中存儲的是receiver,%edx中保存的是Klass
0x00007fffe1022816: mov 0x8(%rcx),%edx 
// LogKlassAlignmentInBytes=0x03,進(jìn)行對齊處理
0x00007fffe1022819: shl $0x3,%rdx
 
// 如下代碼是調(diào)用如下函數(shù)生成的:
__ 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指向第一個(gè)itableOffsetEntry
// 加一個(gè)常量0x1b8是因?yàn)関table之前是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
// 如果相等,則直接跳轉(zhuǎn)到---- found_method ----
0x00007fffe10228db: je 0x00007fffe10228f3
 
// **** search ****
// 檢測%rbx中的值是否為NULL,如果為NULL,
// 那就說明receiver沒有實(shí)現(xiàn)要查詢的接口
0x00007fffe10228dd: test %rbx,%rbx
// 跳轉(zhuǎn)到---- L_no_such_interface ----
0x00007fffe10228e0: je 0x00007fffe1022a8c
0x00007fffe10228e6: add $0x10,%r13
 
0x00007fffe10228ea: mov 0x0(%r13),%rbx
0x00007fffe10228ee: cmp %rbx,%rax
// 如果還是沒有在itableOffsetEntry中找到接口類,
// 則跳轉(zhuǎn)到search繼續(xù)進(jìn)行查找
0x00007fffe10228f1: jne 0x00007fffe10228dd // 跳轉(zhuǎn)到---- search ----
 
// **** found_method ****
 
// 已經(jīng)找到匹配接口的itableOffsetEntry,獲取
// itableOffsetEntry的offset屬性并存儲到%r13d中
0x00007fffe10228f3: mov 0x8(%r13),%r13d
// 通過recv_klass進(jìn)行偏移后找到此接口下聲明
// 的一系列方法的開始位置
0x00007fffe10228f7: mov (%rdx,%r13,1),%rbx

我們需要重點(diǎn)關(guān)注itable的分派邏輯,首先生成了如下匯編:

mov    0x118(%rdx),%r13d

%rdx中存儲的是recv.Klass,獲取Klass中vtable_length屬性的值,有了這個(gè)值,我們就可以計(jì)算出vtable的大小,從而計(jì)算出itable的開始地址。

接著執(zhí)行了如下匯編:

lea    0x1b8(%rdx,%r13,8),%r13

其中的0x1b8表示的是recv.Klass首地址到vtable的距離,這樣最終的%r13指向的是itable的首地址。如下圖所示。

image.png

后面我們就可以開始循環(huán)從itableOffsetEntry中查找匹配的接口了, 如果找到則跳轉(zhuǎn)到found_method,在found_method中,要找到對應(yīng)的itableOffsetEntry的offset,這個(gè)offset指明了接口中定義的方法的存儲位置相對于Klass的偏移量,也就是找到接口對應(yīng)的第一個(gè)itableMethodEntry,因?yàn)?rbx中已經(jīng)存儲了itable的索引,所以根據(jù)這個(gè)索引直接定位對應(yīng)的itableMethodEntry即可,我們現(xiàn)在合起來看如下的2個(gè)匯編:

lea    (%rdx,%rbx,8),%rdx 
...
mov    (%rdx,%r13,1),%rbx

當(dāng)執(zhí)行到如上的第2個(gè)匯編時(shí),%r13存儲的是相對于Klass實(shí)例的偏移,而%rdx在執(zhí)行第1個(gè)匯編時(shí)存儲的是Klass首地址,然后根據(jù)itable索引加上了相對于第1個(gè)itableMethodEntry的偏移,這樣就找到了對應(yīng)的itableMethodEntry。

第6部分

在執(zhí)行如下匯編時(shí),各個(gè)寄存器的值如下:

rbx: Method* to call
rcx: receiver

生成的匯編代碼如下:

0x00007fffe10228fb: test %rbx,%rbx
// 如果本來應(yīng)該存儲Method*的%rbx是空,則表示沒有找到
// 這個(gè)方法,跳轉(zhuǎn)到---- no_such_method ----
0x00007fffe10228fe: je 0x00007fffe1022987 
 
// 保存調(diào)用者的棧頂指針
0x00007fffe1022904: lea 0x8(%rsp),%r13 
0x00007fffe1022909: mov %r13,-0x10(%rbp)
// 跳轉(zhuǎn)到Method::from_interpreted指向的例程并執(zhí)行
0x00007fffe102290d: jmpq *0x58(%rbx) 
 
 
// 省略should_not_reach_here()函數(shù)生成的匯編
 
 
// **** no_such_method ****
// 當(dāng)沒有找到方法時(shí),會跳轉(zhuǎn)到這里執(zhí)行
 
// 彈出調(diào)用prepare_invoke()函數(shù)壓入的返回地址
0x00007fffe1022987: pop %rbx
// 恢復(fù)讓%r13指向bcp
0x00007fffe1022988: mov -0x38(%rbp),%r13
// 恢復(fù)讓%r14指向本地變量表
0x00007fffe102298c: mov -0x30(%rbp),%r14
 
 
// ... 省略通過call_VM()函數(shù)生成的匯編來調(diào)用InterpreterRuntime::throw_abstractMethodError()函數(shù)
// ... 省略調(diào)用should_not_reach_here()函數(shù)生成的匯編代碼
 
// **** no_such_interface ****
 
// 當(dāng)沒有找到匹配的接口時(shí)執(zhí)行的匯編代碼
0x00007fffe1022a8c: pop %rbx
0x00007fffe1022a8d: mov -0x38(%rbp),%r13
0x00007fffe1022a91: mov -0x30(%rbp),%r14
 
// ... 省略通過call_VM()函數(shù)生成的匯編代碼來調(diào)用InterpreterRuntime::throw_IncompatibleClassChangeError()函數(shù)
// ... 省略調(diào)用should_not_reach_here()函數(shù)生成的匯編代碼

對于一些異常的處理這里就不過多介紹了,有興趣的可以看一下相關(guān)匯編代碼的實(shí)現(xiàn)。
`

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容