在前面介绍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; // 解析常量池时,传入的参数pool(根据当前栈中要执行的方法找到对应的常量池)和
// index(常量池缓存项的缓存,还需要映射为原常量池索引)是有值的,根据这两个值能够
// 解析出resolved_klass和要查找的方法名称method_name和方法签名method_signature
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()函数及其调用的相关函数的实现。

1、LinkResolver::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()函数中使用。 

2、LinkResolver::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实例。

推荐阅读:

第1篇-关于JVM运行时,开篇说的简单些

第2篇-JVM虚拟机这样来调用Java主类的main()方法

第3篇-CallStub新栈帧的创建

第4篇-JVM终于开始调用Java主类的main()方法啦

第5篇-调用Java方法后弹出栈帧及处理返回结果

第6篇-Java方法新栈帧的创建

第7篇-为Java方法创建栈帧

第8篇-dispatch_next()函数分派字节码

第9篇-字节码指令的定义

第10篇-初始化模板表

第11篇-认识Stub与StubQueue

第12篇-认识CodeletMark

第13篇-通过InterpreterCodelet存储机器指令片段

第14篇-生成重要的例程

第15章-解释器及解释器生成器

第16章-虚拟机中的汇编器

第17章-x86-64寄存器

第18章-x86指令集之常用指令

第19篇-加载与存储指令(1)

第20篇-加载与存储指令之ldc与_fast_aldc指令(2)

第21篇-加载与存储指令之iload、_fast_iload等(3)

第22篇-虚拟机字节码之运算指令

第23篇-虚拟机字节码指令之类型转换

第24篇-虚拟机对象操作指令之getstatic

第25篇-虚拟机对象操作指令之getfield

第26篇-虚拟机对象操作指令之putstatic

第27篇-虚拟机字节码指令之操作数栈管理指令

第28篇-虚拟机字节码指令之控制转移指令

第29篇-调用Java主类的main()方法

第30篇-main()方法的执行

第31篇-方法调用指令之invokevirtual

第32篇-解析interfacevirtual字节码指令的更多相关文章

  1. 第34篇-解析invokeinterface字节码指令

    与invokevirtual指令类似,当没有对目标方法进行解析时,需要调用LinkResolver::resolve_invoke()函数进行解析,这个函数会调用其它一些函数完成方法的解析,如下图所示 ...

  2. lua源码学习篇四:字节码指令

    在llimits.h文件中定义了指令的类型.其实就是32个字节. typedef lu_int32 Instruction; 上节说到变量最终会存入proto的数组k中,返回的索引放在expdesc ...

  3. 【JVM源码解析】模板解释器解释执行Java字节码指令(上)

    本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ...

  4. 第36篇-return字节码指令

    方法返回的字节码相关指令如下表所示. 0xac ireturn 从当前方法返回int 0xad lreturn 从当前方法返回long 0xae freturn 从当前方法返回float 0xaf d ...

  5. 硬核万字长文,深入理解 Java 字节码指令(建议收藏)

    Java 字节码指令是 JVM 体系中非常难啃的一块硬骨头,我估计有些读者会有这样的疑惑,"Java 字节码难学吗?我能不能学会啊?" 讲良心话,不是我谦虚,一开始学 Java 字 ...

  6. invokedynamic字节码指令

    1. 方法引用和invokedynamic invokedynamic是jvm指令集里面最复杂的一条.本文将从高观点的角度下分析invokedynamic指令是如何实现方法引用(Method refe ...

  7. [四] java虚拟机JVM编译器编译代码简介 字节码指令实例 代码到底编译成了什么形式

      前言简介   前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是 ...

  8. 大话+图说:Java字节码指令——只为让你懂

    前言 随着Java开发技术不断被推到新的高度,对于Java程序员来讲越来越需要具备对更深入的基础性技术的理解,比如Java字节码指令.不然,可能很难深入理解一些时下的新框架.新技术,盲目一味追新也会越 ...

  9. 从字节码指令看重写在JVM中的实现

    Java是解释执行的.包含动态链接的特性.都给解析或执行期间提供了非常多灵活扩展的空间.面向对象语言的继承.封装和多态的特性,在JVM中是怎样进行编译.解析,以及通过字节码指令怎样确定方法调用的版本号 ...

随机推荐

  1. CSS001. 纯CSS实现瀑布流(纵向排序)

    通过 Multi-columns 相关的属性 column-count.column-gap 配合 break-inside 来实现瀑布流布局. 首先对包裹图片的盒子增加样式,column-count ...

  2. jooq使用示例

    一.说明 最近使用的项目,采用了jooq. 通过学习api文档和自我调试,写了一些代码,在此处进行记录. 二.代码 一切尽在代码中-- 参考文档:http://www.jooq.org/doc/3.1 ...

  3. 使用python快速搭建http服务

    python2语法:python -m SimpleHTTPServer python3语法:python -m http.server 在局域网中使用web去访问http:/IP:8000即可 可以 ...

  4. FastAPI(6)- get 请求 - 详解 Query

    可选参数 上一篇文章讲过查询参数可以不是必传的,可以是可选参数 from fastapi import FastAPI from typing import Optional import uvico ...

  5. dede5.7 标题长度限制修改

    我们经常碰到dede标题长度不够用的问题20个字的标题有时候是真的有点短了网上也有些修改长度问题的帖子,但我发现都不完整所以写下来供大家参考下.免得浪费时间 第一步: 修改下面4处文件: dede目录 ...

  6. MySQL修改root密码的多种方法, mysql 导出数据库(包含视图)

    方法1: 用SET PASSWORD命令 mysql -u root mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('newpass ...

  7. PHP-设计模式之-中介者模式

    <?php//中介者模式 -- //抽象中介者abstract class UnitedNationa{ punlic abstract function Declared($message,c ...

  8. 重新嫁接rm命令

    ### 重定义rm命令 #### 定义回收站目录trash_path='~/.trash'# 判断 $trash_path 定义的文件是否存在,如果不存在,那么就创建 $trash_path.if [ ...

  9. AT2390-[AGC016F]Games on DAG【状压dp,SG函数】

    正题 题目链接:https://www.luogu.com.cn/problem/AT2390 解题思路 \(n\)个点的\(DAG\),\(m\)条边可有可无,\(1\)和\(2\)上有石头.求有多 ...

  10. SpringBoot之SpringSecurity权限注解在方法上进行权限认证多种方式

    前言 Spring Security支持方法级别的权限控制.在此机制上,我们可以在任意层的任意方法上加入权限注解,加入注解的方法将自动被Spring Security保护起来,仅仅允许特定的用户访问, ...