关于java:JVM源码解析虚拟机解释执行Java方法上

42次阅读

共计 38625 个字符,预计需要花费 97 分钟才能阅读完成。

本文由 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 性能社区

正文完
 0