第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方法来说,需要通过itable实现动态分派。分派的关键一个汇编语句如下:
mov 0x1b8(%rax,%rbx,8),%rbx
如上是vtable的动态分派逻辑,这个分派逻辑比较简单,之前也介绍过,这里不再介绍。
如果跳转到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
// 其中base=%rdx=recv_klass,index=%r13=scan_temp,scala=8=times_vte_scale,disp=0x1b8=vtable_base
0x00007fffe10228c8: lea 0x1b8(%rdx,%r13,8),%r13
// 其中base=%rdx=recv_klass,index=%rbx=itable_index,scala=8=Address::times_ptr,disp=itentry_off
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()函数生成的汇编代码
对于一些异常的处理这里就不过多介绍了,有兴趣的可以看一下相关汇编代码的实现。
推荐阅读:
第2篇-JVM虚拟机这样来调用Java主类的main()方法
第13篇-通过InterpreterCodelet存储机器指令片段
第20篇-加载与存储指令之ldc与_fast_aldc指令(2)
第21篇-加载与存储指令之iload、_fast_iload等(3)
第33篇-方法调用指令之invokeinterface的更多相关文章
- 第35篇-方法调用指令之invokespecial与invokestatic
这一篇将详细介绍invokespecial和invokestatic字节码指令的汇编实现逻辑 1.invokespecial指令 invokespecial指令的模板定义如下: def(Bytecod ...
- 第31篇-方法调用指令之invokevirtual
invokevirtual字节码指令的模板定义如下: def(Bytecodes::_invokevirtual , ubcp|disp|clvm|____, vtos, vtos, invokevi ...
- 深入理解java虚拟机(十一) 方法调用-解析调用与分派调用
方法调用过程是指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程.我们知道,Class 文件的编译过程中并不包括传统编译中的连接步骤,一切方法调用在 Class 文件调用里面存储的都只是 ...
- 04 JVM是如何执行方法调用的(下)
虚方法调用 Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用会被编译成 invokeinterface 指令.这两种指令,均属于 Java 虚拟机中的虚 ...
- jvm 字节码执行 (一)方法调用
“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上,而虚拟机的执行引擎是 由自己实现的,因此可以自行制定指令集 ...
- JVM方法调用过程
JVM方法调用过程 重载和重写 同一个类中,如果出现多个名称相同,并且参数类型相同的方法,将无法通过编译.因此,想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同.这种方法上的联系就是重载 ...
- JAVA方法调用中的解析与分派
JAVA方法调用中的解析与分派 本文算是<深入理解JVM>的读书笔记,参考书中的相关代码示例,从字节码指令角度看看解析与分派的区别. 方法调用,其实就是要回答一个问题:JVM在执行一个方法 ...
- JVM系列-方法调用的原理
JVM系列-方法调用的原理 最近重新看了一些JVM方面的笔记和资料,收获颇丰,尤其解决了长久以来心中关于JVM方法管理的一些疑问.下面介绍一下JVM中有关方法调用的知识. 目的 方法调用,目的是选择方 ...
- 深入解析多态和方法调用在JVM中的实现
深入解析多态和方法调用在JVM中的实现 1. 什么是多态 多态(polymorphism)是面向对象编程的三大特性之一,它建立在继承的基础之上.在<Java核心技术卷>中这样定义: 一个对 ...
随机推荐
- node实战小例子
第一章 2020-2-6 留言小本子 思路(由于本章没有数据库,客户提交的数据放在全局变量,接收请求用的是bodyParser, padyParser使用方法 app.use(bodyParser.u ...
- Vue+elementUI 创建“回到顶部”组件
1.创建"回到顶部"组件 1 <template> 2 <transition name="el-fade-in"> 3 <div ...
- vue-router路由钩子
路由跳转前后,需要做某些操作,这时就可以使用路由钩子来监听路由的变化. 接收三个参数: to: Route: 即将要进入的目标路由对象 from: Route: 当前导航正要离开的路由 next: F ...
- 计算机网络 -- TCP/IP
画图标准 OSI七层模型 7.应用层 作用:为用户提供软件/接口/界面 interface 协议:OICQ.HTTP.HTTPS.BT/P2P 6.表示层 作用:用于对用户数据进行数据呈现.(数据格式 ...
- 5UCMS判断当前栏目高亮(用于当前所在栏目加背景图片或颜色)
5UCMS判断当前栏目高亮标签 比较简单的是频道页(channel.html): 大类代码: <!--menu:{ $row=10 $table=channel }--> <li { ...
- MapReduce原理深入理解(二)
1.Mapreduce操作不需要reduce阶段 1 import org.apache.hadoop.conf.Configuration; 2 import org.apache.hadoop.f ...
- Linux系列(9) - whoami和whatis
whoami 作用:当前你登录的用户是谁 whatis [命令] 作用:查询[命令]是干嘛的 我们试一下对文件和目录whatis行不行,结果发现不行:但是有没有发现对命令whatis也不行,为什么呢: ...
- python对象引用和垃圾回收
变量="标签" 变量a和变量b引用同一个列表: >>> a = [1, 2, 3] >>> b = a >>> a.appen ...
- 2021牛客暑期多校训练营9C-Cells【LGV引理,范德蒙德行列式】
正题 题目链接:https://ac.nowcoder.com/acm/contest/11260/C 题目大意 一个平面上,\(n\)个起点\((0,a_i)\)分别对应终点\((i,0)\),每次 ...
- P6097-[模板]子集卷积
正题 题目链接:https://www.luogu.com.cn/problem/P6097 题目大意 长度为\(2^n\)的序列\(a,b\)求一个\(c\)满足 \[c_k=\sum_{i|j=k ...