第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; // 解析常量池时,传入的参数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实例。
推荐阅读:
第2篇-JVM虚拟机这样来调用Java主类的main()方法
第13篇-通过InterpreterCodelet存储机器指令片段
第20篇-加载与存储指令之ldc与_fast_aldc指令(2)
第21篇-加载与存储指令之iload、_fast_iload等(3)
第32篇-解析interfacevirtual字节码指令的更多相关文章
- 第34篇-解析invokeinterface字节码指令
与invokevirtual指令类似,当没有对目标方法进行解析时,需要调用LinkResolver::resolve_invoke()函数进行解析,这个函数会调用其它一些函数完成方法的解析,如下图所示 ...
- lua源码学习篇四:字节码指令
在llimits.h文件中定义了指令的类型.其实就是32个字节. typedef lu_int32 Instruction; 上节说到变量最终会存入proto的数组k中,返回的索引放在expdesc ...
- 【JVM源码解析】模板解释器解释执行Java字节码指令(上)
本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ...
- 第36篇-return字节码指令
方法返回的字节码相关指令如下表所示. 0xac ireturn 从当前方法返回int 0xad lreturn 从当前方法返回long 0xae freturn 从当前方法返回float 0xaf d ...
- 硬核万字长文,深入理解 Java 字节码指令(建议收藏)
Java 字节码指令是 JVM 体系中非常难啃的一块硬骨头,我估计有些读者会有这样的疑惑,"Java 字节码难学吗?我能不能学会啊?" 讲良心话,不是我谦虚,一开始学 Java 字 ...
- invokedynamic字节码指令
1. 方法引用和invokedynamic invokedynamic是jvm指令集里面最复杂的一条.本文将从高观点的角度下分析invokedynamic指令是如何实现方法引用(Method refe ...
- [四] java虚拟机JVM编译器编译代码简介 字节码指令实例 代码到底编译成了什么形式
前言简介 前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是 ...
- 大话+图说:Java字节码指令——只为让你懂
前言 随着Java开发技术不断被推到新的高度,对于Java程序员来讲越来越需要具备对更深入的基础性技术的理解,比如Java字节码指令.不然,可能很难深入理解一些时下的新框架.新技术,盲目一味追新也会越 ...
- 从字节码指令看重写在JVM中的实现
Java是解释执行的.包含动态链接的特性.都给解析或执行期间提供了非常多灵活扩展的空间.面向对象语言的继承.封装和多态的特性,在JVM中是怎样进行编译.解析,以及通过字节码指令怎样确定方法调用的版本号 ...
随机推荐
- Nginx-初见
目录 产品出现瓶颈? 什么是Nginx? Nginx作用 正向代理 反向代理 负载均衡策略(Nignx) 轮询 加权轮询 IP hash 动静分离 参考链接 产品出现瓶颈? 项目刚刚上线的时候,并发量 ...
- MySQL实战45讲(16--20)-笔记
目录 16 | "order by"是怎么工作的? 全字段排序 rowid 排序 17 | 如何正确地显示随机消息? 内存临时表 磁盘临时表 随机排序方法 18 | 为什么这些SQ ...
- 用python的pandas读取excel文件中的数据
一.读取Excel文件 使用pandas的read_excel()方法,可通过文件路径直接读取.注意到,在一个excel文件中有多个sheet,因此,对excel文件的读取实际上是读取指定文件.并 ...
- Windows Phone 页面之间参数传递方法
目前对WP7开发正在研究,对页面之间参数传递进行了一个小总结,有不正确的地方,欢迎大家指正.. WP7编程采用的技术是Silverlight,页面之间参数传递的方式主要有 通过NavigationCo ...
- 一键配置tomcat定期日志清理功能
概述 日志文件包含了关于系统中发生的事件的有用信息,在排障过程中或者系统性能分析时经常被用到.对于忙碌的服务器,日志文件大小会增长极快,服务器会很快消耗磁盘空间,这成了个问题.除此之外,处理一个单个的 ...
- 深入学习PHP中的JSON相关函数
在我们当年刚刚上班的那个年代,还全是 XML 的天下,但现在 JSON 数据格式已经是各种应用传输的事实标准了.最近几年开始学习编程开发的同学可能都完全没有接触过使用 XML 来进行数据传输.当然,时 ...
- Java基础系列(8)- 数据类型
数据类型 强类型语言 要求变量的使用合乎规定,所有的变量都必须先定义才能使用.Java是强类型语言. 弱类型语言 变量定义比较随意,比如"12"+3,可以是int型123,也可以是 ...
- python序列类型及一些操作
序列分类 1.按存放的数据类型分类: 容器类型(能存放不同类型的数据):list.tuple.coolections.deque 扁平序列(只能存放一种类型的数据):str.bytes.bytearr ...
- P3352-[ZJOI2016]线段树【dp】
正题 题目链接:https://www.luogu.com.cn/problem/P3352 题目大意 \(n\)个数字的一个序列,每次随机选择一个区间让这个区间所有数等于这个区间的最大值,重复\(q ...
- AT4518-[AGC032C]Three Circuits【欧拉回路】
正题 题目链接:https://www.luogu.com.cn/problem/AT4518 题目大意 给出\(n\)个点\(m\)条边的一张简单无向联通图,求能否把它分成三个可重复点的环. \(1 ...